feat: add all section in admin

This commit is contained in:
Sabda Yagra 2025-09-25 22:46:47 +07:00
parent ce053c264f
commit 72c78caac4
2092 changed files with 26191 additions and 250471 deletions

View File

@ -0,0 +1,697 @@
"use client";
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 { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Book, CheckCheck, Plus, Timer } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { EventContentArg } from "@fullcalendar/core";
import EventModal from "./event-modal";
import { useTranslations } from "next-intl";
import dayjs from "dayjs";
import { getCookiesDecrypt } from "@/lib/utils";
import { CalendarCategory } from "./data";
import { error } from "@/config/swal";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { getAgendaSettingsList } from "@/service/agenda-settings/agenda-setting";
const wait = () => new Promise((resolve) => setTimeout(resolve, 1000));
interface CalendarViewProps {
categories: CalendarCategory[];
}
export type CalendarEvent = {
id: string;
title: string;
start: Date;
end: Date;
createBy: string;
createdByName: string;
isPublish: boolean | null;
allDay: boolean;
extendedProps: {
calendar: string;
description: string;
isPublish?: boolean;
createdByName?: string;
};
};
export interface AgendaSettingsAPIResponse {
id: number;
title: string;
createdByName: string;
description: string;
agendaType: string;
startDate: string;
endDate: string;
isActive: boolean;
isPublish: boolean;
createdAt: string;
updatedAt: string;
createdById: number | null;
}
interface YearlyData {
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 {
monthId: keyof YearlyData;
label: string;
}
interface ListItemProps {
item: AgendaSettingsAPIResponse;
text: string;
createdBy: string;
isPublish: boolean | null;
bgColor: string;
colorList: string[] | null;
}
interface APIResponse {
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<string[]>([]);
const [selectedEventDate, setSelectedEventDate] = useState<Date | null>(null);
const roleId = Number(getCookiesDecrypt("urie")) || 0;
const levelNumber = Number(getCookiesDecrypt("ulne")) || 0;
const userLevelId = Number(getCookiesDecrypt("ulie")) || 0;
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const t = useTranslations("CalendarApp");
const [sheetOpen, setSheetOpen] = useState<boolean>(false);
const [date, setDate] = useState<Date>(new Date());
const [activeView, setActiveView] = useState("listYear");
const [yearlyData, setYearlyData] = useState<YearlyData>({});
const [open, setOpen] = useState(false);
const [selectedEventData, setSelectedEventData] = useState<any>(null);
const [selectedMonth, setSelectedMonth] = useState(
dayjs(new Date(Number(INITIAL_YEAR), Number(INITIAL_MONTH) - 1, 1))
);
useEffect(() => {
if (activeView === "listYear") {
getYearlyEvents();
} else {
getCalendarEvents();
}
}, [activeView, selectedMonth]);
useEffect(() => {
if (categories?.length > 0) {
setSelectedCategory(categories.map((c) => c.value));
}
}, [categories]);
const getCalendarEvents = async () => {
setIsLoading(true);
try {
const res = await getAgendaSettingsList(
selectedMonth?.format("YYYY") || INITIAL_YEAR,
selectedMonth.format("M") || INITIAL_MONTH,
""
);
if (res?.error) {
error(res?.message || "Failed to fetch events");
return;
}
const monthData = res?.data?.data;
if (monthData) {
const allEvents: CalendarEvent[] = [];
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("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 () => {
setIsLoading(true);
try {
const res = await getAgendaSettingsList(
selectedMonth?.format("YYYY") || INITIAL_YEAR,
"",
""
);
if (res?.error) {
error(res?.message || "Failed to fetch yearly events");
return;
}
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);
}
};
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());
const allCategoryId = ["1", "2", "3", "4", "5"];
const hasAllCategories = allCategoryId.every((categoryId) =>
selectedCategory.includes(categoryId)
);
return eventCategories?.some(
(cat: string) =>
selectedCategory.includes(cat) || (hasAllCategories && cat == "0")
);
});
const handleEventClick = (arg: any) => {
setSelectedEventDate(null);
setSelectedEventData(arg);
setSheetOpen(true);
wait().then(() => (document.body.style.pointerEvents = "auto"));
};
const handleCloseModal = () => {
setSheetOpen(false);
setSelectedEventData(null);
setSelectedEventDate(null);
};
const handleDateClick = (arg: any) => {
setSheetOpen(true);
setSelectedEventDate(arg);
setSelectedEventData(null);
wait().then(() => (document.body.style.pointerEvents = "auto"));
};
const handleCategorySelection = (category: string) => {
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);
setSelectedMonth(dayjs(startDateOfDate));
};
const handleViewChange = (viewType: string) => {
setActiveView(viewType);
};
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: 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 eventArg = { event: formattedEvent };
setSelectedEventDate(null);
setSelectedEventData(eventArg);
setSheetOpen(true);
wait().then(() => (document.body.style.pointerEvents = "auto"));
};
const getEventColor = (type: EventType): string => {
const typeSplit = type.split(",");
const firstType = typeSplit[0] as EventType;
const colors: Record<EventType, string> = {
"0": "bg-gray-500",
"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<EventType, string> = {
"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 (
<div
className={`w-full p-2 mb-2 rounded-md text-white text-sm flex justify-between items-stretch ${bgColor}`}
>
<div className="flex-1">
<div className="flex flex-row items-center">
{isPublish === true ? (
<CheckCheck size={15} />
) : (
<Timer size={15} />
)}
<p className="ml-1">{title}</p>
</div>
<p className="ml-1 text-xs text-start mt-2">
Created By: {createdByName}
</p>
</div>
{colorList && colorList.length > 0 && (
<div className="w-3 flex flex-col h-full ml-2 overflow-hidden item-stretch gap-1">
{colorList.map((color: string, index: number) => (
<div key={index} className={`h-[10px] rounded-sm ${color}`}></div>
))}
</div>
)}
</div>
);
};
const handleClassName = (arg: EventContentArg) => {
const eventColor = getEventColor(arg.event.extendedProps.calendar);
return eventColor || "primary";
};
// Components
const ListItem: React.FC<ListItemProps> = ({
item,
text,
createdBy,
isPublish,
bgColor,
colorList,
}) => (
<div
className={`w-full p-2 mb-2 rounded-md text-white text-sm flex justify-between items-stretch cursor-pointer hover:opacity-80 ${bgColor}`}
onClick={() => handleClickListItem(item)}
>
<div className="flex-1">
<div className="flex flex-row items-center">
{isPublish ? <CheckCheck size={15} /> : <Timer size={15} />}
<p className="ml-1">{text}</p>
</div>
<p className="ml-1 text-xs text-start mt-2">Created By: {createdBy}</p>
</div>
{colorList && colorList.length > 0 && (
<div className="w-3 flex flex-col h-full ml-2 overflow-hidden item-stretch gap-1">
{colorList.map((color: string, index: number) => (
<div key={index} className={`h-[10px] rounded-sm ${color}`}></div>
))}
</div>
)}
</div>
);
const MonthCard: React.FC<MonthCardProps> = ({ monthId, label }) => {
const events = yearlyData[monthId] || [];
const displayedEvents = events.slice(0, 3);
const hasMoreEvents = events.length > 3;
return (
<div className="flex-1 bg-white dark:bg-black rounded-lg shadow-sm border border-gray-200 pb-3 mr-1">
<div className="py-3">
<h4 className="font-bold text-center">{label}</h4>
</div>
<div className="px-2">
{events.length === 0 ? (
<div className="mt-1 py-2 rounded-lg bg-white dark:bg-black border border-black dark:border-gray-500">
<p className="text-center">
{t("no-data-yet", { defaultValue: "No Data Yet" })}
</p>
</div>
) : (
<>
{displayedEvents.map((event, index) => (
<ListItem
key={`${event.id}-${index}`}
item={event}
text={event.title}
createdBy={event.createdByName}
isPublish={event.isPublish}
bgColor={getEventColor(event.agendaType as EventType)}
colorList={getEventColorList(event.agendaType as EventType)}
/>
))}
{hasMoreEvents && (
<Popover>
<PopoverTrigger asChild>
<Button className="w-full" size="sm">
Lihat lebih banyak
</Button>
</PopoverTrigger>
<PopoverContent
side="top"
className="p-0 overflow-hidden bg-transparent shadow-none border-none"
>
<Card>
<CardHeader className="bg-default p-2 rounded-t-lg">
<CardTitle className="text-default-foreground text-base py-0">
{label}
</CardTitle>
</CardHeader>
<CardContent className="p-2 text-sm">
<div className="max-h-[300px] overflow-y-auto border rounded-md border-gray-300 p-2">
{events.map((event, index) => (
<ListItem
key={`${event.id}-full-${index}`}
item={event}
text={event.title}
createdBy={event.createdByName}
isPublish={event.isPublish}
bgColor={getEventColor(
event.agendaType as EventType
)}
colorList={getEventColorList(
event.agendaType as EventType
)}
/>
))}
</div>
</CardContent>
</Card>
</PopoverContent>
</Popover>
)}
</>
)}
</div>
</div>
);
};
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 = () => (
<div className="overflow-x-auto overflow-y-auto">
{isLoading ? (
<p>Loading...</p>
) : (
<table className="w-full border-collapse border border-gray-300">
<thead>
<tr className="bg-gray-100 border-b">
<th className="px-4 py-2 text-left">No</th>
<th className="px-4 py-2 text-left">Ticket Number</th>
<th className="px-4 py-2 text-left">Date and Time</th>
<th className="px-4 py-2 text-left">Title</th>
<th className="px-4 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="px-4 py-2">1</td>
<td className="px-4 py-2">MIA - 001</td>
<td className="px-4 py-2">23/01/2025 13:00</td>
<td className="px-4 py-2">Daily Issue 25 Januari 2025</td>
<td className="px-4 py-2">Completed</td>
</tr>
</tbody>
</table>
)}
</div>
);
return (
<>
<div className="grid grid-cols-12 gap-6 divide-x divide-border bg-slate-100 py-4 px-2">
<Card className="col-span-12 lg:col-span-4 2xl:col-span-3 pb-5">
<CardContent className="p-0">
<CardHeader className="border-none mb-2 pt-5">
{/* {[3, 11, 2, 12].includes(roleId) && (
<Button
onClick={handleDateClick}
className="dark:bg-background dark:text-foreground w-full"
>
<Plus className="w-4 h-4 me-1" />
{t("addEvent", { defaultValue: "Add Event" })}
</Button>
)} */}
{/* {[3, 11, 2, 12].includes(roleId) && levelNumber !== 3 && ( */}
<Button
onClick={handleDateClick}
className="dark:bg-background dark:text-foreground w-full border"
>
<Plus className="w-4 h-4 me-1" />
{t("addEvent", { defaultValue: "Add Event" })}
</Button>
{/* )} */}
{/* {roleId === 3 && userLevelId === 216 && ( */}
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="dark:bg-background dark:text-foreground w-full border">
<Book size={15} className="w-4 h-4 mr-3" />
{t("bag-pa-monitoring-results", {
defaultValue: "Bag Pa Monitoring Results",
})}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px] overflow-y-auto max-h-[500px]">
<DialogHeader>
<DialogTitle>
{t("monitoring-results", {
defaultValue: "Monitoring Results",
})}
</DialogTitle>
</DialogHeader>
{getModalContent()}
</DialogContent>
</Dialog>
{/* )} */}
</CardHeader>
<div className="px-3">
<Calendar
mode="single"
selected={date}
onSelect={(selectedDate) => {
if (selectedDate) {
setDate(selectedDate);
handleDateClick(selectedDate);
}
}}
className="rounded-md border w-full p-0 border-none border-black"
/>
</div>
<div className="py-4 text-default-800 font-semibold text-xs uppercase mt-4 mb-2 px-4">
{t("filter", { defaultValue: "Filter" })}
</div>
<ul className="space-y-3 px-4">
<li className="flex gap-3">
<Checkbox
checked={selectedCategory.length === categories.length}
onCheckedChange={() => {
if (selectedCategory.length === categories.length) {
setSelectedCategory([]);
} else {
setSelectedCategory(categories.map((c) => c.value));
}
}}
/>
<Label>All</Label>
</li>
{categories.map((category) => (
<li className="flex gap-3" key={category.value}>
<Checkbox
className={category.className}
id={category.label}
checked={selectedCategory.includes(category.value)}
onCheckedChange={() =>
handleCategorySelection(category.value)
}
/>
<Label htmlFor={category.label}>{category.label}</Label>
</li>
))}
</ul>
</CardContent>
</Card>
<Card className="col-span-12 lg:col-span-8 2xl:col-span-9 pt-5">
<CardContent className="dashcode-app-calendar">
<FullCalendar
plugins={[
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
listPlugin,
]}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "listYear,dayGridMonth,listMonth",
}}
views={{
listYear: {
type: "dayGridMonth",
buttonText: "Year",
},
}}
events={filteredEvents}
editable={true}
rerenderDelay={10}
eventDurationEditable={false}
selectable={true}
selectMirror={true}
droppable={true}
dayMaxEvents={2}
weekends={true}
eventClassNames={handleClassName}
dateClick={handleDateClick}
eventClick={handleEventClick}
initialView="listYear"
eventContent={renderEventContent}
viewDidMount={(info) => handleViewChange(info.view.type)}
datesSet={(info: any) => {
handleDateChange(info.view.currentStart, info.view.currentEnd);
handleViewChange(info.view.type);
}}
viewClassNames={
activeView === "listYear" ? "hide-calendar-grid" : ""
}
/>
{activeView === "listYear" && (
<div className="custom-ui">
{[0, 3, 6, 9].map((startIndex) => (
<div key={startIndex} className="flex gap-1 mt-1">
{months.slice(startIndex, startIndex + 3).map((month) => (
<MonthCard
key={month.id}
monthId={month.id}
label={month.label}
/>
))}
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
<EventModal
open={sheetOpen}
onClose={handleCloseModal}
categories={categories}
event={selectedEventData}
selectedDate={selectedEventDate}
/>
</>
);
};
export default CalendarView;

View File

@ -0,0 +1,151 @@
import dayjs from "dayjs";
const date = new Date();
const prevDay = new Date().getDate() - 1;
const nextDay = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
const INITIAL_YEAR = dayjs().format("YYYY");
const INITIAL_MONTH = dayjs().format("M");
// prettier-ignore
const nextMonth = date.getMonth() === 11 ? new Date(date.getFullYear() + 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() + 1, 1)
// prettier-ignore
const prevMonth = date.getMonth() === 11 ? new Date(date.getFullYear() - 1, 0, 1) : new Date(date.getFullYear(), date.getMonth() - 1, 1)
export const calendarEvents = [
{
id: "event-001-calendar-all-day",
title: "aaaAll Day Event",
start: date,
end: nextDay,
allDay: false,
//className: "warning",
extendedProps: {
calendar: "2",
},
},
{
id: "event-002-meeting-client",
title: "Meeting With Client",
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
allDay: true,
//className: "success",
extendedProps: {
calendar: "1",
},
},
{
id: "event-003-lunch",
title: "Lunch",
allDay: true,
start: new Date(date.getFullYear(), date.getMonth() + 1, -9),
end: new Date(date.getFullYear(), date.getMonth() + 1, -7),
// className: "info",
extendedProps: {
calendar: "3",
},
},
{
id: "event-004-birthday-party",
title: "Birthday Party",
start: new Date(date.getFullYear(), date.getMonth() + 1, -11),
end: new Date(date.getFullYear(), date.getMonth() + 1, -10),
allDay: true,
//className: "primary",
extendedProps: {
calendar: "3",
},
},
{
id: "event-005-birthday-party-2",
title: "Birthday Party",
start: new Date(date.getFullYear(), date.getMonth() + 1, -13),
end: new Date(date.getFullYear(), date.getMonth() + 1, -12),
allDay: true,
// className: "danger",
extendedProps: {
calendar: "2",
},
},
{
id: "event-006-monthly-meeting",
title: "Monthly Meeting",
start: nextMonth,
end: nextMonth,
allDay: true,
//className: "primary",
extendedProps: {
calendar: "5",
},
},
];
export const calendarCategories = [
{
label: "Nasional",
value: "1",
activeClass: "ring-primary-500 bg-primary-500",
className: "group-hover:border-blue-500",
},
{
label: "Polda",
value: "2",
activeClass: "ring-success-500 bg-success-500",
className: " group-hover:border-green-500",
},
{
label: "Polres",
value: "3",
activeClass: "ring-danger-500 bg-danger-500",
className: " group-hover:border-red-500",
},
{
label: "Satker",
value: "4",
activeClass: "ring-yellow-500 bg-yellow-500",
className: " group-hover:border-red-500",
},
{
label: "Internasional",
value: "5",
activeClass: "ring-info-500 bg-info-500",
className: " group-hover:border-cyan-500",
},
];
export const categories = [
{
label: "Nasional",
value: "1",
className:
"data-[state=checked]:bg-yellow-500 data-[state=checked]:ring-yellow-500",
},
{
label: "Polda",
value: "2",
className:
"data-[state=checked]:bg-blue-400 data-[state=checked]:ring-blue-400",
},
{
label: "Polres",
value: "3",
className:
"data-[state=checked]:bg-slate-400 data-[state=checked]:ring-slate-400 ",
},
{
label: "Satker",
value: "4",
className:
"data-[state=checked]:bg-warning data-[state=checked]:ring-warning ",
},
{
label: "Internasional",
value: "5",
className:
"data-[state=checked]:bg-green-500 data-[state=checked]:ring-green-500 ",
},
];
export type CalendarEvent = (typeof calendarEvents)[number];
export type CalendarCategory = (typeof calendarCategories)[number];
export type Category = (typeof categories)[number];

View File

@ -0,0 +1,23 @@
import { cn } from "@/lib/utils";
const ExternalDraggingevent = ({ event }: any) => {
const { title, id, tag } = event;
return (
<div
title={title}
data-id={id}
className="fc-event px-4 py-1.5 bg-default-100 dark:bg-default-300 rounded text-sm flex items-center gap-2 shadow-sm cursor-move" >
<span
className={cn("h-2 w-2 rounded-full block", {
"bg-primary": tag === "business",
"bg-warning": tag === "meeting",
"bg-destructive": tag === "holiday",
"bg-info": tag === "etc",
})}
></span>
<span className="text-sm font-medium text-default-900">{title}</span>
</div>
);
};
export default ExternalDraggingevent;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Calender",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,50 @@
"use client";
import { getEvents, getCategories } from "./utils";
import { calendarEvents, Category } from "./data";
import CalendarView from "./calender-view";
import { getCookiesDecrypt } from "@/lib/utils";
import { useEffect, useState } from "react";
import { CalendarCategory } from "./data";
const CalenderPage = () => {
const [categories, setCategories] = useState<CalendarCategory[]>([]);
const userLevelNumber = Number(getCookiesDecrypt("ulne")) || 0;
const userLevelId = Number(getCookiesDecrypt("ulie")) || 0;
const userParentLevelId = Number(getCookiesDecrypt("uplie")) || 0;
useEffect(() => {
initData();
async function initData() {
const events = await getEvents();
const categories = await getCategories();
let valueShowed: string[] = [];
// if (userLevelNumber == 1) {
// valueShowed = ["0", "1", "2", "3", "4", "5"];
// } else if (userLevelNumber == 2 && userLevelId != 761) {
// valueShowed = ["2", "3"];
// } else if (
// (userLevelNumber == 2 && userLevelId == 761) ||
// (userLevelNumber == 3 && userParentLevelId == 761)
// ) {
// valueShowed = ["4"];
// } else if (userLevelNumber == 3 && userParentLevelId != 761) {
// valueShowed = ["3"];
// }
const formattedCategories = categories
.filter((category: Category) => valueShowed.includes(category.value))
.map((category: Category) => ({
...category,
activeClass: "",
}));
console.log(formattedCategories);
setCategories(formattedCategories);
}
}, []);
return <div>{categories && <CalendarView categories={categories} />}</div>;
};
export default CalenderPage;

View File

@ -0,0 +1,251 @@
"use client";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useEffect, useState } from "react";
import { getUserLevelForAssignments } from "@/service/task";
const FormSchema = z.object({
items: z.array(z.string()).refine((value) => value.some((item) => item), {
message: "Required",
}),
});
interface UnitType {
id: number;
name: string;
subDestination: { id: number; name: string }[] | null;
}
export function UnitMapping(props: {
unit: "Polda" | "Satker" | "Polres";
sendDataToParent: (data: string[]) => void;
isDetail: boolean;
initData?: string[];
}) {
const { unit, sendDataToParent, isDetail } = props;
const [unitList, setUnitList] = useState<UnitType[]>([]);
const [satkerList, setSatkerList] = useState<{ id: number; name: string }[]>(
[]
);
const [polresList, setPolresList] = useState<{ id: number; name: string }[]>(
[]
);
const [isOpen, setIsOpen] = useState(false);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
items: props.initData ? props.initData : [],
},
});
useEffect(() => {
async function initState() {
const response = await getUserLevelForAssignments();
const list: UnitType[] = response?.data?.data?.list ?? [];
setupUnit(list);
// console.log("list", list);
}
initState();
}, []);
// Selalu punya array
const unitType = form.watch("items") ?? [];
// ---- FIX: case-insensitive + flatMap semua node ----
const setupUnit = (data: UnitType[] = []) => {
const norm = (v?: string) => (v ?? "").toUpperCase();
const poldaNodes = data.filter((a) => norm(a.name).includes("POLDA"));
const satkerNodes = data.filter((a) => norm(a.name).includes("SATKER"));
// POLDA level (UnitType)
setUnitList(poldaNodes);
// SATKER adalah gabungan semua subDestination dari node SATKER
const allSatker = satkerNodes.flatMap((n) => n.subDestination ?? []) ?? [];
setSatkerList(allSatker);
// POLRES adalah gabungan semua subDestination dari node POLDA
const allPolres = poldaNodes.flatMap((n) => n.subDestination ?? []);
setPolresList(allPolres);
};
// ---- FIX: jangan true saat list kosong ----
const isAllUnitChecked =
unitList.length > 0 &&
unitList.every((item) => unitType.includes(String(item.id)));
const isAllSatkerChecked =
satkerList.length > 0 &&
satkerList.every((item) => unitType.includes(String(item.id)));
const isAllPolresChecked =
polresList.length > 0 &&
polresList.every((item) => unitType.includes(String(item.id)));
useEffect(() => {
sendDataToParent(form.getValues("items"));
}, [unitType]);
// helper untuk ambil id-ids per kategori aktif
const currentIds = () => {
if (unit === "Polda") return unitList.map((i) => String(i.id));
if (unit === "Satker") return satkerList.map((i) => String(i.id));
return polresList.map((i) => String(i.id));
};
const allCheckedByUnit =
unit === "Polda"
? isAllUnitChecked
: unit === "Satker"
? isAllSatkerChecked
: isAllPolresChecked;
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<a
onClick={() => setIsOpen(true)}
className="text-primary cursor-pointer text-xs mr-3"
>
Pilih {unit}
</a>
</DialogTrigger>
<DialogContent size="md" className="h-[500px] overflow-y-auto">
<DialogHeader>
<DialogTitle>{unit}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form className="flex flex-col gap-2">
<div className="flex items-center gap-3">
<Checkbox
id={`all-${unit}`}
checked={allCheckedByUnit}
onCheckedChange={(checked) => {
const ids = currentIds();
if (checked) {
// gabungkan supaya kategori lain tidak hilang
const merged = Array.from(new Set([...unitType, ...ids]));
form.setValue("items", merged, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
});
} else {
// hapus hanya id kategori aktif
const filtered = unitType.filter((v) => !ids.includes(v));
form.setValue("items", filtered, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
});
}
}}
/>
<label
htmlFor={`all-${unit}`}
className="text-sm text-black uppercase"
>
SEMUA {unit}
</label>
</div>
<FormField
control={form.control}
name="items"
render={() => (
<FormItem
className={`grid ${
unit === "Polda"
? "grid-cols-2"
: unit === "Satker"
? "grid-cols-3"
: "grid-cols-4"
} gap-2`}
>
{(unit === "Polda"
? unitList
: unit === "Satker"
? satkerList
: polresList
)?.map((item: any) => (
<FormField
key={item.id}
control={form.control}
name="items"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value?.includes(String(item.id))}
onCheckedChange={(checked) => {
if (checked) {
form.setValue(
"items",
Array.from(
new Set([
...(field.value ?? []),
String(item.id),
])
),
{
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
}
);
} else {
form.setValue(
"items",
(field.value ?? []).filter(
(v: string) => v !== String(item.id)
),
{
shouldDirty: true,
shouldTouch: true,
shouldValidate: true,
}
);
}
}}
/>
</FormControl>
<p className="text-sm text-black">{item.name}</p>
</FormItem>
)}
/>
))}
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,11 @@
import { calendarEvents, categories } from "./data";
// get events
export const getEvents = async () => {
return calendarEvents;
};
// get categories
export const getCategories = async () => {
return categories;
};

View File

@ -153,15 +153,15 @@ const useCategoryColumns = () => {
<DropdownMenuContent align="end">
<Link
href={`/admin/categories/detail/${row.original.id}`}
className="hover:text-black"
className="hover:text-black cursor-pointer"
>
<DropdownMenuItem>
<Eye className="w-4 h-4 mr-2" /> View
<Eye className="w-4 h-4 mr-2" /> Detail
</DropdownMenuItem>
</Link>
<Link
href={`/admin/categories/update/${row.original.id}`}
className="hover:text-black"
className="hover:text-black cursor-pointer"
>
<DropdownMenuItem>
<SquarePen className="w-4 h-4 mr-2" /> Edit

View File

@ -113,7 +113,7 @@ const TableCategories = () => {
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent hover:bg-transparent"
className="bg-transparent hover:bg-transparent cursor-pointer"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
@ -121,12 +121,12 @@ const TableCategories = () => {
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/admin/categories/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<Eye className="w-4 h-4 mr-1.5" /> View
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none cursor-pointer">
<Eye className="w-4 h-4 mr-1.5 " /> Detail
</DropdownMenuItem>
</Link>
<Link href={`/admin/categories/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none cursor-pointer">
<SquarePen className="w-4 h-4 mr-1.5" /> Edit
</DropdownMenuItem>
</Link>

View File

@ -0,0 +1,15 @@
import CategoriesDetailForm from "@/components/form/categories/categories-detail-form";
import FormImageDetail from "@/components/form/content/image/image-detail-form";
const CategoriesDetailPage = async () => {
return (
<div>
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<CategoriesDetailForm />{" "}
</div>
</div>
);
};
export default CategoriesDetailPage;

View File

@ -33,14 +33,11 @@ import Swal from "sweetalert2";
import { createCategories } from "@/service/categories/categories";
const FormSchema = z.object({
createdById: z.coerce.number(),
description: z.string().min(1, "Deskripsi wajib diisi"),
oldCategoryId: z.coerce.number(),
parentId: z.coerce.number(),
slug: z.string().min(1, "Slug wajib diisi"),
statusId: z.coerce.number(),
tags: z.string().min(1, "Tags wajib diisi"),
title: z.string().min(1, "Nama kategori wajib diisi"),
slug: z.string().min(1, "Slug wajib diisi"),
description: z.string().min(1, "Deskripsi wajib diisi"),
statusId: z.coerce.number().default(1), // biar default aktif
tags: z.string().optional(), // opsional aja kalau belum wajib
});
// helper untuk bikin slug otomatis
@ -59,10 +56,7 @@ const ReactTableImagePage = () => {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
createdById: 1,
description: "",
oldCategoryId: 0,
parentId: 0,
slug: "",
statusId: 1,
tags: "",
@ -70,7 +64,6 @@ const ReactTableImagePage = () => {
},
});
// Auto generate slug ketika title berubah
const handleTitleChange = (value: string) => {
form.setValue("title", value);
const autoSlug = slugify(value);
@ -87,8 +80,6 @@ const ReactTableImagePage = () => {
loading();
try {
const response = await createCategories(data);
// ✅ Tutup swal "Loading" sebelum tampil swal baru
MySwal.close();
if (response?.error) {
@ -104,7 +95,7 @@ const ReactTableImagePage = () => {
title: "Gagal",
text: message || "Gagal membuat kategori",
confirmButtonColor: "#d33",
timer: 2000, // ⏱ otomatis hilang setelah 2 detik
timer: 2000,
showConfirmButton: false,
});
return;
@ -115,11 +106,12 @@ const ReactTableImagePage = () => {
title: "Sukses",
text: "Kategori berhasil dibuat",
confirmButtonColor: "#3085d6",
timer: 2000, // ⏱ otomatis hilang setelah 2 detik
timer: 2000,
showConfirmButton: false,
});
setIsOpen(false);
window.location.reload();
} catch (err: any) {
MySwal.close();
@ -128,7 +120,7 @@ const ReactTableImagePage = () => {
title: "Terjadi kesalahan",
text: err?.message || "Coba lagi nanti",
confirmButtonColor: "#d33",
timer: 2000, // ⏱ otomatis hilang setelah 2 detik
timer: 2000,
showConfirmButton: false,
});
}
@ -228,7 +220,7 @@ const ReactTableImagePage = () => {
/>
{/* Tags */}
<FormField
{/* <FormField
control={form.control}
name="tags"
render={({ field }) => (
@ -243,10 +235,10 @@ const ReactTableImagePage = () => {
<FormMessage />
</FormItem>
)}
/>
/> */}
{/* createdById, oldCategoryId, parentId, statusId → bisa hidden atau number input */}
<FormField
{/* <FormField
control={form.control}
name="createdById"
render={({ field }) => (
@ -258,9 +250,9 @@ const ReactTableImagePage = () => {
<FormMessage />
</FormItem>
)}
/>
/> */}
<FormField
{/* <FormField
control={form.control}
name="statusId"
render={({ field }) => (
@ -272,7 +264,7 @@ const ReactTableImagePage = () => {
<FormMessage />
</FormItem>
)}
/>
/> */}
<DialogFooter>
<Button type="submit" color="primary">

View File

@ -0,0 +1,217 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Link } from "@/components/navigation";
import { useTranslations } from "next-intl";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { error } from "@/config/swal";
import { deleteSchedule } from "@/service/service/schedule/schedule";
const useTableColumns = (props: { selectedTypeSchedule: string }) => {
const t = useTranslations("Table"); // Panggil di dalam hook
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "title",
header: t("title", { defaultValue: "Title" }),
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "startDate",
header: t("start-date", { defaultValue: "Start Date" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("startDate")}</span>
),
},
{
accessorKey: "endDate",
header: t("end-date", { defaultValue: "End Date" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("endDate")}</span>
),
},
{
accessorKey: "time",
header: t("time", { defaultValue: "Time" }),
cell: ({ row }: { row: { original: any } }) => {
console.log("Row Original Data:", row.original);
const { startTime, endTime } = row.original;
return (
<span className="whitespace-nowrap">
{startTime || "N/A"} - {endTime || "N/A"}
</span>
);
},
},
{
accessorKey: "address",
header: t("address", { defaultValue: "Address" }),
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const address: string = row.getValue("address");
return (
<span className="whitespace-nowrap">
{address.length > 50 ? `${address.slice(0, 40)}...` : address}
</span>
);
},
},
{
accessorKey: "statusName",
header: "Status",
cell: ({ row }) => {
const statusColors: Record<string, string> = {
diterima: "bg-green-100 text-green-600",
"menunggu review": "bg-orange-100 text-orange-600",
};
// Mengambil `statusName` dari data API
const status = row.getValue("statusName") as string;
const statusName = status?.toLocaleLowerCase(); // Ubah ke huruf kecil
// Gunakan `statusName` untuk pencocokan
const statusStyles =
statusColors[statusName] || "bg-gray-100 text-gray-600";
return (
<Badge
className={cn("rounded-full px-5 whitespace-nowrap", statusStyles)}
>
{status} {/* Tetap tampilkan nilai asli */}
</Badge>
);
},
},
{
accessorKey: "speaker",
header: t("speaker", { defaultValue: "Speaker" }),
cell: ({ row }: { row: { original: any } }) => {
console.log("Row Original Data:", row.original);
const { speakerTitle, speakerName } = row.original;
return (
<span className="whitespace-nowrap">
{speakerTitle || ""} {speakerName || ""}
</span>
);
},
},
{
accessorKey: "uploaderName",
header: t("source", { defaultValue: "Source" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("uploaderName")}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
async function doDelete(id: any) {
// loading();
const response = await deleteSchedule(id);
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();
}
});
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/contributor/schedule/live-report/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
Detail
</DropdownMenuItem>
</Link>
<Link
href={`/contributor/schedule/live-report/update/${row.original.id}?scheduleType=${props.selectedTypeSchedule}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => doDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,388 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UploadIcon,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./columns";
import { CardHeader, CardTitle } from "@/components/ui/card";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import useTableColumns from "./columns";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { paginationSchedule } from "@/service/service/schedule/schedule";
const LiveReportTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations("Schedule");
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [selectedType, setSelectedType] = React.useState<string>("1");
const columns = useTableColumns({ selectedTypeSchedule: selectedType });
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page, showData, search, statusFilter, selectedType]);
async function fetchData() {
try {
const res = await paginationSchedule(
showData,
page - 1,
"",
search,
statusFilter
);
const data = res?.data?.data ?? {};
const contentData = Array.isArray(data.content) ? data.content : [];
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements ?? 0);
setTotalPage(data?.totalPages ?? 1);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleStatusCheckboxChange = (statusId: number) => {
const updatedFilter = statusFilter.includes(statusId)
? statusFilter.filter((id) => id !== statusId)
: [...statusFilter, statusId];
setStatusFilter(updatedFilter);
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
};
return (
<div className="border-b border-solid border-default-200 mb-6 bg-slate-100 rounded-2xl">
<CardHeader>
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("live-report", { defaultValue: "Live Report" })}{" "}
{t("schedule", { defaultValue: "Schedule" })}
</div>
<div className="flex-none">
<Link href={"/admin/schedule/live-report/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-schedule", { defaultValue: "Create Schedule" })}
</Button>
</Link>
</div>
</div>
</CardTitle>
</CardHeader>
<div className="w-full overflow-x-auto">
<div className="flex flex-col sm:flex-row md:flex-row lg:flex-row justify-between items-center px-3 gap-y-2">
<div className="w-full sm:w-[150px] md:w-[250px] lg:w-[250px]">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Title..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</InputGroup>
</div>
<div className="grid grid-cols-2 w-full md:w-fit md:flex lg:flex-row items-center gap-3">
<div className="w-full md:w-fit">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline" className="w-full">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="border dark:bg-transparent border-black dark:border dark:border-white rounded-md w-full md:w-fit">
<Select
value={selectedType}
onValueChange={(value) => setSelectedType(value)}
>
<SelectTrigger className="w-full md:w-[150px] text-black dark:text-white">
<SelectValue placeholder="Tipe" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Tipe</SelectLabel>
<SelectItem value="1">Konferensi Pers</SelectItem>
<SelectItem value="2">Event</SelectItem>
<SelectItem value="3">Pers Rilis</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="w-full md:w-fit">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-full md:ml-auto"
size="md"
>
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[150px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-1"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-1" className="text-sm">
Menunggu Review
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Diterima
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center w-full md:w-fit">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="md:ml-auto w-full"
size="md"
>
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<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>
);
};
export default LiveReportTable;

View File

@ -0,0 +1,16 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormLiveReport from "@/components/form/schedule/live-report-form";
const LiveReportCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormLiveReport />
</div>
</div>
);
};
export default LiveReportCreatePage;

View File

@ -0,0 +1,16 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormDetailLiveReport from "@/components/form/schedule/live-report-detail-form";
const LiveReportDetailPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDetailLiveReport />
</div>
</div>
);
};
export default LiveReportDetailPage;

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Press Conference",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,20 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import LiveReportTable from "./component/live-report-table";
const LiveReportPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<Card>
<CardContent className="p-0">
<LiveReportTable />
</CardContent>
</Card>
</div>
</div>
);
};
export default LiveReportPage;

View File

@ -0,0 +1,16 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormUpdateLiveReport from "@/components/form/schedule/live-report-update-form";
const LiveReportUpdatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormUpdateLiveReport />
</div>
</div>
);
};
export default LiveReportUpdatePage;

View File

@ -0,0 +1,278 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import columns from "./columns";
import useTableColumns from "./columns";
import { getTicketingCollaborationPagination } from "@/service/service/communication/communication";
const CollaborationTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState<string>("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page, showData]);
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
async function fetchData() {
try {
const res = await getTicketingCollaborationPagination(
page - 1,
showData,
search
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
return (
<div className="w-full overflow-x-auto">
<div className="flex justify-between items-center">
<div className=" flex flex-row items-center gap-2">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
</InputGroup>
</div>
<div className="flex flex-row items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<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>
);
};
export default CollaborationTable;

View File

@ -0,0 +1,125 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { Link, useRouter } from "@/i18n/routing";
import { useTranslations } from "next-intl";
const useTableColumns = () => {
const t = useTranslations("Table"); // Panggil di dalam hook
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => <span> {row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: t("question", { defaultValue: "Question" }),
cell: ({ row }) => (
<span className="normal-case"> {row.getValue("title")}</span>
),
},
{
accessorKey: "commentFromUserName",
header: t("sender", { defaultValue: "Sender" }),
cell: ({ row }) => (
<span className="normal-case">
{row.getValue("commentFromUserName")}
</span>
),
},
{
accessorKey: "Type",
header: t("type", { defaultValue: "Type" }),
cell: ({ row }) => {
const type = row.original.type;
return <span className="normal-case">{type?.name || "N/A"}</span>;
},
},
{
accessorKey: "createdAt",
header: t("time", { defaultValue: "Time" }),
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "isActive",
header: "Status",
cell: ({ row }) => {
const isActive = row.getValue("isActive") as boolean; // Ambil nilai isActive
const status = isActive ? "Open" : "Closed"; // Tentukan teks berdasarkan isActive
const statusStyles = isActive
? "bg-green-100 text-green-600" // Gaya untuk "Open"
: "bg-red-100 text-red-600"; // Gaya untuk "Closed"
return (
<Badge className={`rounded-full px-5 ${statusStyles}`}>
{status}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
cell: ({ row }) => {
const router = useRouter();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<DropdownMenuItem
onClick={() =>
router.push(
`/shared/communication/collaboration/detail/${row.original.id}`
)
}
className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none items-center"
>
<Eye className="w-4 h-4 me-1.5" />
Detail
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,15 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormCollaboration from "@/components/form/communication/collaboration-form";
const CollaborationCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormCollaboration />
</div>
</div>
);
};
export default CollaborationCreatePage;

View File

@ -0,0 +1,139 @@
"use client";
import { Button } from "@/components/ui/button";
import Select, { MultiValue } from "react-select";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useEffect, useState } from "react";
import { Separator } from "@/components/ui/separator";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, loading } from "@/config/swal";
import { useParams } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
import { stringify } from "querystring";
import { getCuratorUser, saveCollaborationTeams } from "@/service/service/communication/communication";
const assigneeOptions = [
{ value: "mahedi", label: "Mahedi Amin", image: "/images/avatar/av-1.svg" },
{ value: "sovo", label: "Sovo Haldar", image: "/images/avatar/av-2.svg" },
{
value: "rakibul",
label: "Rakibul Islam",
image: "/images/avatar/av-3.svg",
},
{ value: "pritom", label: "Pritom Miha", image: "/images/avatar/av-4.svg" },
];
export default function UsersCard(props: { team: any; fetchData: () => void }) {
const params = useParams();
const id = params?.id;
const [openSearch, setOpenSearch] = useState(false);
const [selectedUsers, setSelectedUsers] = useState<any>([]);
const [allUser, setAllUser] = useState([]);
const MySwal = withReactContent(Swal);
const { toast } = useToast();
useEffect(() => {
async function getUser() {
const res = await getCuratorUser();
if (res?.data !== null) {
const rawUser = res?.data?.data?.content;
const optionArr: any = [];
rawUser.map((option: any) => {
optionArr.push({
id: option.id,
label: option.username,
value: option.id,
});
});
setAllUser(optionArr);
}
}
getUser();
}, []);
const inviteHandle = () => {
MySwal.fire({
title: "Undang Pengguna Ke Kolom Diskusi?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
inviteUser();
}
});
};
async function inviteUser() {
const userId: any = [];
selectedUsers?.map((user: any) => {
userId.push(user.id);
});
loading();
const res = await saveCollaborationTeams(String(id), userId);
if (res?.error) {
toast({ title: stringify(res?.message) });
return false;
}
close();
toast({ title: "Berhasil menambahkan user" });
props.fetchData();
}
return (
<div className="flex flex-col bg-white p-4 rounded-t-xl w-[30%]">
<div className="flex justify-between items-center mb-3">
<div className="flex flex-row gap-3 items-center">
<p>Kolaborator</p>
<p className="p-2 rounded-full bg-slate-300 w-5 h-5 flex justify-center items-center">
{props?.team?.length}
</p>
</div>
<a onClick={() => setOpenSearch(!openSearch)}>
<Icon icon="material-symbols:search" width={24} />
</a>
</div>
{openSearch && (
<div className="flex flex-col">
<Select
className="react-select my-2 transition-shadow"
classNamePrefix="select"
options={allUser}
isMulti
onChange={(selectedOption) => setSelectedUsers(selectedOption)}
placeholder="Select users"
/>
{selectedUsers?.length > 0 && (
<div className="flex justify-end">
<Button color="primary" size="sm" onClick={inviteHandle}>
Invie
</Button>
</div>
)}
</div>
)}
<div className="mt-3 flex flex-col gap-2">
{props?.team?.map((list: any) => (
<div key={list.id} className="flex flex-col">
<div className="flex flex-row gap-2 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div className="flex flex-col">
<p className="text-sm">{list?.fullname}</p>
<p className="text-xs">{list?.username}</p>
</div>
</div>
<Separator className="my-2" />
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,196 @@
"use client";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/use-toast";
import { close, loading } from "@/config/swal";
import {
getLocaleTimestamp,
htmlToString,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams } from "next/navigation";
import { stringify } from "querystring";
import { useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import UsersCard from "./component/users-card";
import { deleteCollabDiscussion, getCollabDiscussion, getTicketCollaborationTeams, saveCollabDiscussion } from "@/service/service/communication/communication";
export default function CollaborationPage() {
const params = useParams();
const id = params?.id;
const { toast } = useToast();
const [detailTickets, setDetailTickets] = useState<any>();
const [teams, setTeams] = useState([]);
const [listDiscussion, setListDiscussion] = useState([]);
const [replyValue, setReplyValue] = useState("");
const MySwal = withReactContent(Swal);
useEffect(() => {
async function getCollabTeam() {
if (id != undefined) {
const res = await getTicketCollaborationTeams(String(id));
if (res?.data !== null) {
console.log("response teams:", res);
setDetailTickets(res?.data?.data?.tickets);
setTeams(res?.data?.data?.userTeam);
console.log("userteam", res?.data?.data?.userTeam);
}
}
}
initState();
getCollabTeam();
}, [id]);
async function initState() {
if (id != undefined) {
loading();
const responseGet = await getCollabDiscussion(String(id));
console.log("get res data", responseGet?.data?.data);
close();
setListDiscussion(responseGet?.data?.data);
}
}
async function deleteDataSuggestion(dataId: string | number) {
loading();
const response = await deleteCollabDiscussion(dataId);
console.log(response);
toast({
title: "Sukses hapus",
});
setReplyValue("");
close();
initState();
}
async function sendDiscussionParent() {
if (replyValue?.length > 0) {
loading();
const data = {
ticketId: id,
message: replyValue,
parentId: null,
};
const response = await saveCollabDiscussion(data);
if (response?.error) {
toast({
title: stringify(response?.message),
});
return false;
}
toast({
title: "Sukses Komen",
});
setReplyValue("");
close();
initState();
}
}
return (
<div>
<SiteBreadcrumb />
<div className="flex flex-row gap-4 pt-6">
<div className="flex flex-col gap-4 w-[70%]">
<div className="bg-white flex flex-col rounded-t-xl">
<div className="flex flex-row gap-2 bg-slate-300 rounded-t-xl p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div className="flex flex-col gap-1">
<div className="flex flex-row">
<p className="text-sm">
<span className="font-semibold">
{detailTickets?.commentFromUserName}
</span>{" "}
mengirimkan komentar untuk
</p>
<a
href={detailTickets?.feed?.permalink_url}
className="font-weight-bold"
>
{textEllipsis(detailTickets?.feed?.message, 25)}
</a>
</div>
<p className="text-xs">
{" "}
{`${new Date(detailTickets?.createdAt).getDate()}-${
new Date(detailTickets?.createdAt).getMonth() + 1
}-${new Date(
detailTickets?.createdAt
).getFullYear()} ${new Date(
detailTickets?.createdAt
).getHours()}:${new Date(
detailTickets?.createdAt
).getMinutes()}`}
</p>
</div>
</div>
<div className="p-4"> {htmlToString(detailTickets?.message)}</div>
</div>
<div className="bg-white rounded-b-xl flex flex-col p-4">
<p className="font-bold text-sm">{listDiscussion?.length} Respon</p>
<Separator className="my-2" />
<div className="flex flex-col gap-2 max-h-[360px]">
{listDiscussion?.length > 1 ? (
listDiscussion?.map((data: any) => (
<div key={data?.id} className="flex flex-col ">
<div className="flex flex-row gap-2 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div className="flex flex-col w-full">
<div className="flex justify-between items-center">
<p className="text-sm font-semibold">
{data?.messageFrom?.fullname}
</p>
<p className="text-xs">
{getLocaleTimestamp(new Date(data?.createdAt))}
</p>
</div>
<div className="flex justify-between items-center">
<p className="text-sm">{data?.message}</p>
<a
onClick={() => deleteDataSuggestion(data?.id)}
className="text-destructive cursor-pointer text-xs"
>
Hapus
</a>
</div>
</div>
</div>
<Separator className="my-2" />
</div>
))
) : (
<p>Belum Ada Tanggapan</p>
)}
</div>
<Textarea
value={replyValue}
onChange={(e) => setReplyValue(e.target.value)}
/>
<div className="flex justify-end py-3">
<Button
className="w-fit"
color="primary"
size="sm"
type="button"
onClick={sendDiscussionParent}
>
Kirim
</Button>
</div>
</div>
</div>{" "}
<UsersCard team={teams} fetchData={() => initState()} />
</div>
</div>
);
}

View File

@ -0,0 +1,120 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { Link } from "@/components/navigation";
import { useTranslations } from "next-intl";
const useTableColumns = () => {
const t = useTranslations("Table"); // Panggil di dalam hook
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => <span> {row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: t("question", { defaultValue: "Question" }),
cell: ({ row }) => (
<span className="normal-case"> {row.getValue("title")}</span>
),
},
{
accessorKey: "commentFromUserName",
header: t("sender", { defaultValue: "Sender" }),
cell: ({ row }) => (
<span className="normal-case">
{row.getValue("commentFromUserName")}
</span>
),
},
{
accessorKey: "type",
header: t("type", { defaultValue: "Type" }),
cell: ({ row }) => {
const type = row.original.type; // Akses properti category
return <span className="normal-case">{type?.name || "N/A"}</span>;
},
},
{
accessorKey: "createdAt",
header: t("time", { defaultValue: "Time" }),
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "isActive",
header: "Status",
cell: ({ row }) => {
const isActive = row.getValue("isActive") as boolean; // Ambil nilai isActive
const status = isActive ? "Open" : "Closed"; // Tentukan teks berdasarkan isActive
const statusStyles = isActive
? "bg-green-100 text-green-600" // Gaya untuk "Open"
: "bg-red-100 text-red-600"; // Gaya untuk "Closed"
return (
<Badge className={`rounded-full px-5 ${statusStyles}`}>
{status}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/shared/communication/escalation/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,286 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import useTableColumns from "./columns";
import { getTicketingEscalationPagination } from "@/service/service/communication/communication";
const EscalationTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState<string>("");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
async function fetchData() {
try {
const res = await getTicketingEscalationPagination(
page - 1,
Number(showData),
search
);
const data = res?.data?.data;
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value); // Perbarui state search
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
};
return (
<div className="w-full overflow-x-auto">
<div className="flex justify-between items-center">
<div className=" flex flex-row items-center gap-2">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
</InputGroup>
</div>
<div className="flex flex-row items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<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>
);
};
export default EscalationTable;

View File

@ -0,0 +1,19 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
import FormDetailInternal from "@/components/form/communication/internal-detail-form";
import FormDetailEscalation from "@/components/form/communication/escalation-detail-form";
const EscalationDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDetailEscalation />
</div>
</div>
);
};
export default EscalationDetailPage;

View File

@ -0,0 +1,116 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { Link } from "@/components/navigation";
import { useTranslations } from "next-intl";
const useTableColumns = () => {
const t = useTranslations("Table"); // Panggil di dalam hook
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => <span> {row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: t("question", { defaultValue: "Question" }),
cell: ({ row }) => (
<span className="normal-case"> {row.getValue("title")}</span>
),
},
{
accessorKey: "createdBy",
header: t("sender", { defaultValue: "Sender" }),
cell: ({ row }) => {
const createdBy = row.original.createdBy; // Akses properti category
return (
<span className="normal-case">{createdBy?.fullname || "N/A"}</span>
);
},
},
{
accessorKey: "sendTo",
header: t("sendto", { defaultValue: "Sendto" }),
cell: ({ row }) => {
const sendTo = row.original.sendTo; // Akses properti category
return <span className="normal-case">{sendTo?.fullname || "N/A"}</span>;
},
},
{
accessorKey: "createdAt",
header: t("time", { defaultValue: "Time" }),
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link
href={`/shared/communication/internal/detail/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
<Link
href={`/shared/communication/internal/update/${row.original.id}`}
>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none">
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,286 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
UploadIcon,
} from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import useTableColumns from "./columns";
import { listTicketingInternal } from "@/service/service/communication/communication";
const InternalTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState<string>("");
const userId = getCookiesDecrypt("uie");
const userLevelId = getCookiesDecrypt("ulie");
const roleId = getCookiesDecrypt("urie");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
setPagination({
pageIndex: 0,
pageSize: Number(showData),
});
}, [page, showData]);
let typingTimer: any;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
fetchData();
}
async function fetchData() {
try {
const res = await listTicketingInternal(
page - 1,
Number(showData),
search
);
const data = res?.data?.data;
const contentData = data?.content ?? [];
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
return (
<div className="w-full overflow-x-auto ">
<div className="flex justify-between items-center">
<div className="mt-3 flex flex-row items-center gap-2">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Title..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
/>
</InputGroup>
</div>
<div className="flex flex-row items-center gap-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="20">
1 - 20 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="25">
1 - 25 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<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>
);
};
export default InternalTable;

View File

@ -0,0 +1,15 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormInternal from "@/components/form/communication/internal-form";
const InternalCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormInternal />
</div>
</div>
);
};
export default InternalCreatePage;

View File

@ -0,0 +1,18 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
import FormDetailInternal from "@/components/form/communication/internal-detail-form";
const InternalDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDetailInternal />
</div>
</div>
);
};
export default InternalDetailPage;

View File

@ -0,0 +1,19 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
import FormDetailInternal from "@/components/form/communication/internal-detail-form";
import FormEditInternal from "@/components/form/communication/internal-edit-form";
const InternalUpdatePage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormEditInternal />
</div>
</div>
);
};
export default InternalUpdatePage;

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Communication",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,87 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import CollaborationTable from "./collaboration/components/collabroation-table";
import EscalationTable from "./escalation/components/escalation-table";
import { useState } from "react";
import { Link, useRouter } from "@/i18n/routing";
import { PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import InternalTable from "./internal/components/internal-table";
import { useTranslations } from "next-intl";
const CommunicationPage = () => {
const [tab, setTab] = useState("Pertanyaan Internal");
const t = useTranslations("Communication");
return (
<div>
<SiteBreadcrumb />
<div className="w-full overflow-x-auto bg-slate-100 dark:bg-black p-4 rounded-sm space-y-3 border">
<div className="flex justify-between py-3">
<p className="text-lg font-bold">{tab}</p>
{tab === "Pertanyaan Internal" && (
<Link href="/admin/shared/communication/internal/create">
<Button color="primary" size="md">
<PlusIcon size={18} className="mr-2" />
{t("new-question", { defaultValue: "New Question" })}
</Button>
</Link>
)}
{tab === "Kolaborasi" && (
<Link href="/admin/shared/communication/collaboration/create">
<Button color="primary" size="md">
<PlusIcon size={18} className="mr-2" />
{t("new-collaboration", { defaultValue: "New Collaboration" })}
</Button>
</Link>
)}
</div>
<div className="flex flex-wrap gap-1 border-2 rounded-md w-fit mb-5">
<Button
rounded="md"
onClick={() => setTab("Pertanyaan Internal")}
className={`
${
tab === "Pertanyaan Internal"
? "bg-black text-white "
: "bg-white text-black "
}`}
>
{t("internal-questions", { defaultValue: "Internal Questions" })}
</Button>
<Button
rounded="md"
onClick={() => setTab("Eskalasi")}
className={` hover:text-white
${
tab === "Eskalasi"
? "bg-black text-white "
: "bg-white text-black "
}`}
>
{t("escalation", { defaultValue: "Escalation" })}
</Button>
<Button
rounded="md"
onClick={() => setTab("Kolaborasi")}
className={` hover:text-white
${
tab === "Kolaborasi"
? "bg-black text-white "
: "bg-white text-black "
}`}
>
{t("collaboration", { defaultValue: "Collaboration" })}
</Button>
</div>
{tab === "Pertanyaan Internal" && <InternalTable />}
{tab === "Eskalasi" && <EscalationTable />}
{tab === "Kolaborasi" && <CollaborationTable />}
</div>
</div>
);
};
export default CommunicationPage;

View File

@ -0,0 +1,273 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2, Upload } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { Link } from "@/components/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { error } from "@/lib/swal";
import { deleteMedia } from "@/service/content/content";
import { deleteContest, publishContest } from "@/service/contest/contest";
import { useTranslations } from "next-intl";
const useTableColumns = () => {
const t = useTranslations("Table"); // Panggil di dalam hook
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("no")}
</h4>
</div>
</div>
),
},
{
accessorKey: "hastagCode",
header: t("code", { defaultValue: "Code" }),
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4
className="text-sm font-bold
text-default-600 whitespace-nowrap mb-1"
>
{row.getValue("hastagCode")}
</h4>
</div>
</div>
),
},
{
accessorKey: "theme",
header: t("title", { defaultValue: "Title" }),
cell: ({ row }) => (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{row.getValue("theme")}
</h4>
</div>
</div>
),
},
{
accessorKey: "duration",
header: t("duration", { defaultValue: "Duration" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">{row.getValue("duration")}</span>
),
},
{
accessorKey: "targetOutput",
header: t("target-output", { defaultValue: "Target Output" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("targetOutput")}
</span>
),
},
{
accessorKey: "targetParticipantTopLevel",
header: t("target-participant", { defaultValue: "Target Participant" }),
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("targetParticipantTopLevel")}
</span>
),
},
{
accessorKey: "isPublishForAll", // Bisa menggunakan ini untuk membaca default data
header: "Status",
cell: ({
row,
}: {
row: {
original: { isPublishForAll?: boolean; isPublishForMabes?: boolean };
};
}) => {
const userRoleId: number = Number(getCookiesDecrypt("urie"));
const userLevelNumber: number = Number(getCookiesDecrypt("ulne"));
const isPublishForAll: boolean = Boolean(row.original.isPublishForAll);
const isPublishForMabes: boolean = Boolean(
row.original.isPublishForMabes
);
const isPublish: boolean = isPublishForMabes === true;
const isPending: boolean =
(userRoleId === 3 && userLevelNumber === 1 && !isPublishForAll) ||
((userRoleId === 11 || userRoleId === 12) && !isPublishForMabes);
return (
<Badge
className={`whitespace-nowrap px-2 py-1 rounded-full ${
isPending
? "bg-orange-100 text-orange-600"
: isPublish
? "bg-green-100 text-green-600"
: "bg-blue-100 text-blue-600"
}`}
>
{isPending ? "Pending" : isPublish ? "Publish" : "Terkirim"}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
cell: ({ row }) => {
const MySwal = withReactContent(Swal);
const userRoleId = Number(getCookiesDecrypt("urie"));
const userLevelId = Number(getCookiesDecrypt("ulie"));
const userLevelNumber = Number(getCookiesDecrypt("ulne"));
async function doPublish(id: any) {
// loading();
// const data = {
// id,
// };
const response = await publishContest(id);
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 handlePublishContest = (id: any) => {
MySwal.fire({
title: "Apakah anda ingin publish Lomba?",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Ya",
cancelButtonText: "Tidak",
}).then((result) => {
if (result.isConfirmed) {
doPublish(id);
}
});
};
async function doDelete(id: any) {
// loading();
const data = {
id,
};
const response = await deleteContest(id);
if (response?.error) {
error(response.message);
return false;
}
success();
}
const handleDeleteContest = (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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
{((userRoleId == 11 || userRoleId == 12) &&
row?.original?.isPublishForMabes != true) ||
(userRoleId == 3 &&
userLevelNumber == 1 &&
row?.original?.isPublishForAll != true) ? (
<DropdownMenuItem
className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none"
onClick={() => handlePublishContest(row.original.id)}
>
<Upload className="w-4 h-4 me-1.5" />
Publish
</DropdownMenuItem>
) : (
""
)}
<Link href={`/shared/contest/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
<Link href={`/shared/contest/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => handleDeleteContest(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,339 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import useTableColumns from "./columns";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Label } from "@/components/ui/label";
import { listContest } from "@/service/service/contest/contest";
const TaskTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("50");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState<string>("");
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [page, showData, search, statusFilter]);
async function fetchData() {
try {
const res = await listContest(search, showData, page - 1);
const data = res?.data?.data;
let contentData = data?.content;
if (statusFilter.length > 0) {
contentData = contentData.filter((item: any) => {
const { isPublishForAll, isPublishForMabes } = item;
const status = (() => {
if (isPublishForAll && isPublishForMabes) return 1; // Publish
if (!isPublishForAll && isPublishForMabes) return 3; // Terkirim
return 2; // Pending
})();
return statusFilter.includes(status);
});
}
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleStatusCheckboxChange = (status: number) => {
setStatusFilter((prev) =>
prev.includes(status)
? prev.filter((item) => item !== status)
: [...prev, status]
);
};
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value); // Set filter tabel
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col sm:flex-row md:flex-row lg:flex-row justify-between items-center px-3 gap-y-2">
<div className="w-full sm:w-[150px] md:w-[250px] lg:w-[250px]">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</InputGroup>
</div>
<div className="flex flex-row items-center gap-3">
<div className="">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[150px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
</div>
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-1"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-1" className="text-sm">
Publish
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
Pending
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-3"
className="mr-2"
checked={statusFilter.includes(3)}
onChange={() => handleStatusCheckboxChange(3)}
/>
<label htmlFor="status-3" className="text-sm">
Terkirim
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<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>
);
};
export default TaskTable;

View File

@ -0,0 +1,15 @@
import FormContestDetail from "@/components/form/contest/contest-detail-form";
import SiteBreadcrumb from "@/components/site-breadcrumb";
const ContestDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormContestDetail />
</div>
</div>
);
};
export default ContestDetailPage;

View File

@ -0,0 +1,17 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormContestDetail from "@/components/form/contest/contest-detail-form";
const ContestDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormContestDetail />
</div>
</div>
);
};
export default ContestDetailPage;

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Contest",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,52 @@
"use client";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import ContestTable from "./components/contest-table";
import { Link } from "@/components/navigation";
import { Button } from "@/components/ui/button";
import { UploadIcon } from "lucide-react";
import { getCookiesDecrypt } from "@/lib/utils";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
const ContestPage = () => {
const [userLevelId, setUserLevelId] = useState<any>(null);
const t = useTranslations("Contest");
useEffect(() => {
setUserLevelId(Number(getCookiesDecrypt("ulie")));
}, []);
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("table", { defaultValue: "Table" })} {t("contest", { defaultValue: "Contest" })}
</div>
{userLevelId !== 776 && userLevelId !== null && (
<div className="flex-none">
<Link href={"/shared/contest/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-contest", { defaultValue: "Create Contest" })}
</Button>
</Link>
</div>
)}
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<ContestTable />
</CardContent>
</Card>
</div>
</div>
);
};
export default ContestPage;

View File

@ -0,0 +1,15 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormContestUpdate from "@/components/form/contest/contest-update-form";
const ContestUpdatePage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormContestUpdate />
</div>
</div>
);
};
export default ContestUpdatePage;

View File

@ -0,0 +1,78 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { getListContent } from "@/service/landing/landing";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const VideoSliderPage = () => {
const [newContent, setNewContent] = useState<any>();
const [selectedTab, setSelectedTab] = useState("video");
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = params?.locale;
const type = "popular";
useEffect(() => {
initFetch();
});
const initFetch = async () => {
const request = {
sortBy: type == "popular" ? "clickCount" : "createdAt",
contentTypeId: "2",
};
const response = await getListContent(request);
console.log("category", response);
setNewContent(response?.data?.data?.content);
};
return (
<div className="mx-3 px-5">
<Carousel className="w-full max-w-7xl mx-auto ">
<CarouselContent>
{newContent?.map((video: any) => (
<CarouselItem
key={video?.id}
className="md:basis-1/2 lg:basis-1/3 "
>
<Link
href={generateLocalizedPath(
`/video/detail/${video?.id}`,
String(locale)
)}
className="relative group rounded-md overflow-hidden shadow-md hover:shadow-lg"
>
<img
src={video?.thumbnailLink}
className="w-full h-32 lg:h-60 object-cover group-hover:scale-100 transition-transform duration-300 rounded-md"
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-r from-black to-slate-500 text-white p-2">
<h1 className="text-sm font-semibold truncate">
{video?.title}
</h1>
<p className="flex flex-row items-center text-sm gap-2">
{formatDateToIndonesian(new Date(video?.createdAt))}{" "}
{video?.timezone ? video?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" />{" "}
{video.clickCount}{" "}
</p>
</div>
</Link>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
);
};
export default VideoSliderPage;

View File

@ -0,0 +1,89 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const AudioSliderPage = () => {
const [newContent, setNewContent] = useState<any>();
const [selectedTab, setSelectedTab] = useState("video");
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = params?.locale;
const type = "popular";
useEffect(() => {
initFetch();
});
const initFetch = async () => {
const request = {
sortBy: type == "popular" ? "clickCount" : "createdAt",
contentTypeId: "4",
};
const response = await getListContent(request);
console.log("category", response);
setNewContent(response?.data?.data?.content);
};
return (
<div className="mx-3 px-5">
<Carousel>
<CarouselContent>
{newContent?.map((audio: any) => (
<CarouselItem key={audio?.id} className="md:basis-1/2 lg:basis-1/3">
<div className="flex flex-row gap-6">
<a
href="#"
className="flex flex-col sm:flex-row items-center bg-white dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full"
>
<div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-8 lg:h-16">
<svg
width="32"
height="34"
viewBox="0 0 32 34"
fill="null"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.404 0.452014C23.7033 0.35857 24.0204 0.336816 24.3297 0.388509C24.639 0.440203 24.9318 0.563895 25.1845 0.749599C25.4371 0.935304 25.6426 1.17782 25.7843 1.45756C25.9259 1.73731 25.9998 2.04644 26 2.36001V14.414C25.3462 14.2296 24.6766 14.1064 24 14.046V8.36001L10 12.736V27C10 28.1264 9.6197 29.2197 8.92071 30.1029C8.22172 30.9861 7.24499 31.6075 6.14877 31.8663C5.05255 32.125 3.90107 32.0061 2.88089 31.5287C1.86071 31.0514 1.03159 30.2435 0.52787 29.2361C0.024152 28.2286 -0.124656 27.0806 0.105556 25.9781C0.335768 24.8755 0.931513 23.883 1.79627 23.1613C2.66102 22.4396 3.74413 22.031 4.87009 22.0017C5.99606 21.9724 7.09893 22.3242 8.00001 23V6.73601C7.99982 6.30956 8.13596 5.8942 8.38854 5.55059C8.64112 5.20698 8.99692 4.9531 9.40401 4.82601L23.404 0.452014ZM10 10.64L24 6.26601V2.36001L10 6.73601V10.64ZM5.00001 24C4.20436 24 3.44129 24.3161 2.87869 24.8787C2.31608 25.4413 2.00001 26.2044 2.00001 27C2.00001 27.7957 2.31608 28.5587 2.87869 29.1213C3.44129 29.6839 4.20436 30 5.00001 30C5.79566 30 6.55872 29.6839 7.12133 29.1213C7.68394 28.5587 8.00001 27.7957 8.00001 27C8.00001 26.2044 7.68394 25.4413 7.12133 24.8787C6.55872 24.3161 5.79566 24 5.00001 24ZM32 25C32 27.387 31.0518 29.6761 29.364 31.364C27.6761 33.0518 25.387 34 23 34C20.6131 34 18.3239 33.0518 16.636 31.364C14.9482 29.6761 14 27.387 14 25C14 22.6131 14.9482 20.3239 16.636 18.6361C18.3239 16.9482 20.6131 16 23 16C25.387 16 27.6761 16.9482 29.364 18.6361C31.0518 20.3239 32 22.6131 32 25ZM27.47 24.128L21.482 20.828C21.3298 20.7443 21.1583 20.7016 20.9846 20.7043C20.8108 20.707 20.6408 20.7549 20.4912 20.8433C20.3416 20.9317 20.2176 21.0576 20.1315 21.2086C20.0453 21.3595 20 21.5302 20 21.704V28.304C20 28.4778 20.0453 28.6486 20.1315 28.7995C20.2176 28.9504 20.3416 29.0763 20.4912 29.1647C20.6408 29.2531 20.8108 29.301 20.9846 29.3037C21.1583 29.3064 21.3298 29.2638 21.482 29.18L27.47 25.88C27.6268 25.7937 27.7575 25.6669 27.8486 25.5128C27.9397 25.3587 27.9877 25.183 27.9877 25.004C27.9877 24.825 27.9397 24.6493 27.8486 24.4952C27.7575 24.3412 27.6268 24.2143 27.47 24.128Z"
fill="white"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm">
{formatDateToIndonesian(new Date(audio?.createdAt))}{" "}
{audio?.timezone ? audio?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" />{" "}
{audio?.clickCount}{" "}
</div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{textEllipsis(audio.title, 50)}
</div>
</div>
</a>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
);
};
export default AudioSliderPage;

View File

@ -0,0 +1,79 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const ImageSliderPage = () => {
const [newContent, setNewContent] = useState<any>();
const [selectedTab, setSelectedTab] = useState("video");
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = params?.locale;
const type = "popular";
useEffect(() => {
initFetch();
});
const initFetch = async () => {
const request = {
sortBy: type == "popular" ? "clickCount" : "createdAt",
contentTypeId: "3",
};
const response = await getListContent(request);
console.log("category", response);
setNewContent(response?.data?.data?.content);
};
return (
<div className="mx-3 px-5">
<Carousel>
<CarouselContent>
{newContent?.map((image: any) => (
<CarouselItem key={image?.id} className="md:basis-1/2 lg:basis-1/3">
<Link
href={generateLocalizedPath(
`/image/detail/${image?.id}`,
String(locale)
)}
className="relative group rounded-md overflow-hidden shadow-md hover:shadow-lg"
>
<img
src={image?.thumbnailLink}
className="w-full h-32 lg:h-60 object-cover group-hover:scale-100 transition-transform duration-300 rounded-md"
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-r from-black to-slate-500 text-white p-2">
<h1 className="text-sm font-semibold truncate">
{image?.title}
</h1>
<p className="flex flex-row items-center text-sm gap-2">
{formatDateToIndonesian(new Date(image?.createdAt))}{" "}
{image?.timezone ? image?.timezone : "WIB"}|{" "}
<Icon icon="formkit:eye" width="15" height="15" />{" "}
{image?.clickCount}{" "}
</p>
</div>
</Link>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
);
};
export default ImageSliderPage;

View File

@ -0,0 +1,103 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const TeksSliderPage = () => {
const [newContent, setNewContent] = useState<any>();
const [selectedTab, setSelectedTab] = useState("video");
const router = useRouter();
const pathname = usePathname();
const params = useParams();
const locale = params?.locale;
const type = "popular";
useEffect(() => {
initFetch();
});
const initFetch = async () => {
const request = {
sortBy: type == "popular" ? "clickCount" : "createdAt",
contentTypeId: "1",
};
const response = await getListContent(request);
console.log("category", response);
setNewContent(response?.data?.data?.content);
};
return (
<div className="mx-3 px-5">
<Carousel>
<CarouselContent>
{newContent?.map((text: any) => (
<CarouselItem key={text?.id} className="md:basis-1/2 lg:basis-1/3">
<div className="md:basis-1/2 lg:basis-1/3">
<a
href="#"
className="flex flex-col bg-yellow-500 sm:flex-row items-center dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full h-[100px]"
>
<div className="flex items-center justify-center rounded-lg w-16 h-2 lg:h-16">
<svg
width="28"
height="34"
viewBox="0 0 28 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.6665 17.4167C5.6665 17.0851 5.7982 16.7672 6.03262 16.5328C6.26704 16.2984 6.58498 16.1667 6.9165 16.1667C7.24802 16.1667 7.56597 16.2984 7.80039 16.5328C8.03481 16.7672 8.1665 17.0851 8.1665 17.4167C8.1665 17.7482 8.03481 18.0661 7.80039 18.3005C7.56597 18.535 7.24802 18.6667 6.9165 18.6667C6.58498 18.6667 6.26704 18.535 6.03262 18.3005C5.7982 18.0661 5.6665 17.7482 5.6665 17.4167ZM6.9165 21.1667C6.58498 21.1667 6.26704 21.2984 6.03262 21.5328C5.7982 21.7672 5.6665 22.0851 5.6665 22.4167C5.6665 22.7482 5.7982 23.0661 6.03262 23.3005C6.26704 23.535 6.58498 23.6667 6.9165 23.6667C7.24802 23.6667 7.56597 23.535 7.80039 23.3005C8.03481 23.0661 8.1665 22.7482 8.1665 22.4167C8.1665 22.0851 8.03481 21.7672 7.80039 21.5328C7.56597 21.2984 7.24802 21.1667 6.9165 21.1667ZM5.6665 27.4167C5.6665 27.0851 5.7982 26.7672 6.03262 26.5328C6.26704 26.2984 6.58498 26.1667 6.9165 26.1667C7.24802 26.1667 7.56597 26.2984 7.80039 26.5328C8.03481 26.7672 8.1665 27.0851 8.1665 27.4167C8.1665 27.7482 8.03481 28.0661 7.80039 28.3005C7.56597 28.535 7.24802 28.6667 6.9165 28.6667C6.58498 28.6667 6.26704 28.535 6.03262 28.3005C5.7982 28.0661 5.6665 27.7482 5.6665 27.4167ZM11.9165 16.1667C11.585 16.1667 11.267 16.2984 11.0326 16.5328C10.7982 16.7672 10.6665 17.0851 10.6665 17.4167C10.6665 17.7482 10.7982 18.0661 11.0326 18.3005C11.267 18.535 11.585 18.6667 11.9165 18.6667H21.0832C21.4147 18.6667 21.7326 18.535 21.9671 18.3005C22.2015 18.0661 22.3332 17.7482 22.3332 17.4167C22.3332 17.0851 22.2015 16.7672 21.9671 16.5328C21.7326 16.2984 21.4147 16.1667 21.0832 16.1667H11.9165ZM10.6665 22.4167C10.6665 22.0851 10.7982 21.7672 11.0326 21.5328C11.267 21.2984 11.585 21.1667 11.9165 21.1667H21.0832C21.4147 21.1667 21.7326 21.2984 21.9671 21.5328C22.2015 21.7672 22.3332 22.0851 22.3332 22.4167C22.3332 22.7482 22.2015 23.0661 21.9671 23.3005C21.7326 23.535 21.4147 23.6667 21.0832 23.6667H11.9165C11.585 23.6667 11.267 23.535 11.0326 23.3005C10.7982 23.0661 10.6665 22.7482 10.6665 22.4167ZM11.9165 26.1667C11.585 26.1667 11.267 26.2984 11.0326 26.5328C10.7982 26.7672 10.6665 27.0851 10.6665 27.4167C10.6665 27.7482 10.7982 28.0661 11.0326 28.3005C11.267 28.535 11.585 28.6667 11.9165 28.6667H21.0832C21.4147 28.6667 21.7326 28.535 21.9671 28.3005C22.2015 28.0661 22.3332 27.7482 22.3332 27.4167C22.3332 27.0851 22.2015 26.7672 21.9671 26.5328C21.7326 26.2984 21.4147 26.1667 21.0832 26.1667H11.9165ZM26.3565 11.0233L16.6415 1.31C16.6157 1.28605 16.5885 1.26378 16.5598 1.24333C16.5392 1.22742 16.5192 1.21074 16.4998 1.19333C16.3852 1.08512 16.2632 0.984882 16.1348 0.893332C16.0922 0.865802 16.0476 0.841298 16.0015 0.819999L15.9215 0.779999L15.8382 0.731666C15.7482 0.679999 15.6565 0.626665 15.5615 0.586665C15.2296 0.454104 14.8783 0.376423 14.5215 0.356665C14.4885 0.354519 14.4557 0.350625 14.4232 0.344999C14.3779 0.338012 14.3323 0.334114 14.2865 0.333332H3.99984C3.11578 0.333332 2.26794 0.684521 1.64281 1.30964C1.01769 1.93476 0.666504 2.78261 0.666504 3.66667V30.3333C0.666504 31.2174 1.01769 32.0652 1.64281 32.6904C2.26794 33.3155 3.11578 33.6667 3.99984 33.6667H23.9998C24.8839 33.6667 25.7317 33.3155 26.3569 32.6904C26.982 32.0652 27.3332 31.2174 27.3332 30.3333V13.38C27.333 12.496 26.9817 11.6483 26.3565 11.0233ZM24.8332 30.3333C24.8332 30.5543 24.7454 30.7663 24.5891 30.9226C24.4328 31.0789 24.2208 31.1667 23.9998 31.1667H3.99984C3.77882 31.1667 3.56686 31.0789 3.41058 30.9226C3.2543 30.7663 3.1665 30.5543 3.1665 30.3333V3.66667C3.1665 3.44565 3.2543 3.23369 3.41058 3.07741C3.56686 2.92113 3.77882 2.83333 3.99984 2.83333H13.9998V10.3333C13.9998 11.2174 14.351 12.0652 14.9761 12.6904C15.6013 13.3155 16.4491 13.6667 17.3332 13.6667H24.8332V30.3333ZM16.4998 4.70166L22.9632 11.1667H17.3332C17.1122 11.1667 16.9002 11.0789 16.7439 10.9226C16.5876 10.7663 16.4998 10.5543 16.4998 10.3333V4.70166Z"
fill="black"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm">
{formatDateToIndonesian(new Date(text?.createdAt))}{" "}
{text?.timezone ? text?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" />{" "}
{text?.clickCount}{" "}
</div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{textEllipsis(text?.title, 50)}
</div>
<div className="flex gap-2 items-center text-sm text-red-500 dark:text-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 512 512"
>
<path
fill="#f00"
d="M224 30v256h-64l96 128l96-128h-64V30zM32 434v48h448v-48z"
/>
</svg>
Download Dokumen
</div>
</div>
</a>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
);
};
export default TeksSliderPage;

View File

@ -0,0 +1,129 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const AudioAll = () => {
const [audioData, setAudioData] = useState<any>();
const [displayAudio, setDisplayAudio] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState("");
useEffect(() => {
initFetch();
}, [page, limit, search]);
useEffect(() => {
if (audioData?.length > 0) {
shuffleAndSetVideos();
const interval = setInterval(shuffleAndSetVideos, 5000);
return () => clearInterval(interval); // Cleanup interval on unmount
}
}, [audioData]);
const initFetch = async () => {
const response = await listCuratedContent(search, limit, page - 1, 4, "1");
console.log(response);
const data = response?.data?.data;
const contentData = data?.content;
setAudioData(contentData);
};
const shuffleAndSetVideos = () => {
const shuffled = shuffleArray([...audioData]);
setDisplayAudio(shuffled.slice(0, 3));
};
const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
return (
<div className="mx-3 px-5">
<div className=" grid grid-cols-1 gap-6 ">
{displayAudio?.map((audio: any) => (
<Link
href={`/shared/curated-content//giat-routine/audio/detail/${audio.id}`}
key={audio?.id}
className="flex flex-col sm:flex-row items-center hover:scale-100 transition-transform duration-300 bg-white dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full"
>
<div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-16 h-16">
<svg
width="32"
height="34"
viewBox="0 0 32 34"
fill="null"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.404 0.452014C23.7033 0.35857 24.0204 0.336816 24.3297 0.388509C24.639 0.440203 24.9318 0.563895 25.1845 0.749599C25.4371 0.935304 25.6426 1.17782 25.7843 1.45756C25.9259 1.73731 25.9998 2.04644 26 2.36001V14.414C25.3462 14.2296 24.6766 14.1064 24 14.046V8.36001L10 12.736V27C10 28.1264 9.6197 29.2197 8.92071 30.1029C8.22172 30.9861 7.24499 31.6075 6.14877 31.8663C5.05255 32.125 3.90107 32.0061 2.88089 31.5287C1.86071 31.0514 1.03159 30.2435 0.52787 29.2361C0.024152 28.2286 -0.124656 27.0806 0.105556 25.9781C0.335768 24.8755 0.931513 23.883 1.79627 23.1613C2.66102 22.4396 3.74413 22.031 4.87009 22.0017C5.99606 21.9724 7.09893 22.3242 8.00001 23V6.73601C7.99982 6.30956 8.13596 5.8942 8.38854 5.55059C8.64112 5.20698 8.99692 4.9531 9.40401 4.82601L23.404 0.452014ZM10 10.64L24 6.26601V2.36001L10 6.73601V10.64ZM5.00001 24C4.20436 24 3.44129 24.3161 2.87869 24.8787C2.31608 25.4413 2.00001 26.2044 2.00001 27C2.00001 27.7957 2.31608 28.5587 2.87869 29.1213C3.44129 29.6839 4.20436 30 5.00001 30C5.79566 30 6.55872 29.6839 7.12133 29.1213C7.68394 28.5587 8.00001 27.7957 8.00001 27C8.00001 26.2044 7.68394 25.4413 7.12133 24.8787C6.55872 24.3161 5.79566 24 5.00001 24ZM32 25C32 27.387 31.0518 29.6761 29.364 31.364C27.6761 33.0518 25.387 34 23 34C20.6131 34 18.3239 33.0518 16.636 31.364C14.9482 29.6761 14 27.387 14 25C14 22.6131 14.9482 20.3239 16.636 18.6361C18.3239 16.9482 20.6131 16 23 16C25.387 16 27.6761 16.9482 29.364 18.6361C31.0518 20.3239 32 22.6131 32 25ZM27.47 24.128L21.482 20.828C21.3298 20.7443 21.1583 20.7016 20.9846 20.7043C20.8108 20.707 20.6408 20.7549 20.4912 20.8433C20.3416 20.9317 20.2176 21.0576 20.1315 21.2086C20.0453 21.3595 20 21.5302 20 21.704V28.304C20 28.4778 20.0453 28.6486 20.1315 28.7995C20.2176 28.9504 20.3416 29.0763 20.4912 29.1647C20.6408 29.2531 20.8108 29.301 20.9846 29.3037C21.1583 29.3064 21.3298 29.2638 21.482 29.18L27.47 25.88C27.6268 25.7937 27.7575 25.6669 27.8486 25.5128C27.9397 25.3587 27.9877 25.183 27.9877 25.004C27.9877 24.825 27.9397 24.6493 27.8486 24.4952C27.7575 24.3412 27.6268 24.2143 27.47 24.128Z"
fill="white"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm">
{formatDateToIndonesian(new Date(audio?.createdAt))}{" "}
{audio?.timezone ? audio?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" /> 518
</div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{audio?.title}
</div>
</div>
<div className="flex items-center justify-center gap-3">
<div className="mt-2">
<img src="/assets/wave.svg" className="w-80" />
</div>
<div className="flex flex-row items-center justify-center text-gray-500 dark:text-gray-400">
<img
src="/assets/audio-icon.png"
alt="#"
className="flex items-center justify-center"
/>
<div className="flex mx-2 items-center justify-center">
{audio?.duration}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
>
<path
fill="#f00"
d="M7.707 10.293a1 1 0 1 0-1.414 1.414l3 3a1 1 0 0 0 1.414 0l3-3a1 1 0 0 0-1.414-1.414L11 11.586V6h5a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5v5.586zM9 4a1 1 0 0 1 2 0v2H9z"
/>
</svg>
</div>
</div>
</Link>
))}
</div>
</div>
);
};
export default AudioAll;

View File

@ -0,0 +1,73 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
import AudioAll from "./audio";
const AudioAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Audio</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio</Label>
</div>
<div className="px-5 my-5">
<AudioAll />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default AudioAllPage;

View File

@ -0,0 +1,111 @@
"use client";
import { Link } from "@/components/navigation";
import { Card } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { hasData } from "jquery";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const AudioSliderPage = () => {
const [audioData, setAudioData] = useState<any>();
const [displayAudio, setDisplayAudio] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState("");
const [hasData, setHasData] = useState(false);
useEffect(() => {
initFetch();
}, [page, limit, search]);
const initFetch = async () => {
const response = await listCuratedContent(search, limit, page - 1, 4, "1");
console.log(response);
const data = response?.data?.data;
const contentData = data?.content;
setHasData(displayAudio && displayAudio.length > 0);
setDisplayAudio(contentData);
};
const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
return (
<div className="w-full px-2">
{displayAudio.length > 0 && (
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-semibold">Audio</h2>
<Link
href={"/shared/curated-content/giat-routine/video/all"}
className="text-sm text-gray-500 hover:text-gray-700 flex items-center"
>
Lihat Semua <Icon icon="lucide:arrow-right" className="ml-1" />
</Link>
</div>
<Carousel className="w-full">
<CarouselContent>
{displayAudio.map((audio, index) => (
<CarouselItem key={index} className="md:basis-1/3 lg:basis-1/3">
<div className="p-2">
<Card className=" shadow-md rounded-lg overflow-hidden">
<Link
href={`/shared/curated-content/giat-routine/audio/detail/${audio.id}`}
>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center justify-center bg-red-500 text-white rounded-lg w-12 h-12 mx-3 my-3">
<svg
width="28"
height="28"
viewBox="0 0 32 34"
fill="null"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.404 0.452014C23.7033 0.35857 24.0204 0.336816 24.3297 0.388509C24.639 0.440203 24.9318 0.563895 25.1845 0.749599C25.4371 0.935304 25.6426 1.17782 25.7843 1.45756C25.9259 1.73731 25.9998 2.04644 26 2.36001V14.414C25.3462 14.2296 24.6766 14.1064 24 14.046V8.36001L10 12.736V27C10 28.1264 9.6197 29.2197 8.92071 30.1029C8.22172 30.9861 7.24499 31.6075 6.14877 31.8663C5.05255 32.125 3.90107 32.0061 2.88089 31.5287C1.86071 31.0514 1.03159 30.2435 0.52787 29.2361C0.024152 28.2286 -0.124656 27.0806 0.105556 25.9781C0.335768 24.8755 0.931513 23.883 1.79627 23.1613C2.66102 22.4396 3.74413 22.031 4.87009 22.0017C5.99606 21.9724 7.09893 22.3242 8.00001 23V6.73601C7.99982 6.30956 8.13596 5.8942 8.38854 5.55059C8.64112 5.20698 8.99692 4.9531 9.40401 4.82601L23.404 0.452014ZM10 10.64L24 6.26601V2.36001L10 6.73601V10.64ZM5.00001 24C4.20436 24 3.44129 24.3161 2.87869 24.8787C2.31608 25.4413 2.00001 26.2044 2.00001 27C2.00001 27.7957 2.31608 28.5587 2.87869 29.1213C3.44129 29.6839 4.20436 30 5.00001 30C5.79566 30 6.55872 29.6839 7.12133 29.1213C7.68394 28.5587 8.00001 27.7957 8.00001 27C8.00001 26.2044 7.68394 25.4413 7.12133 24.8787C6.55872 24.3161 5.79566 24 5.00001 24ZM32 25C32 27.387 31.0518 29.6761 29.364 31.364C27.6761 33.0518 25.387 34 23 34C20.6131 34 18.3239 33.0518 16.636 31.364C14.9482 29.6761 14 27.387 14 25C14 22.6131 14.9482 20.3239 16.636 18.6361C18.3239 16.9482 20.6131 16 23 16C25.387 16 27.6761 16.9482 29.364 18.6361C31.0518 20.3239 32 22.6131 32 25ZM27.47 24.128L21.482 20.828C21.3298 20.7443 21.1583 20.7016 20.9846 20.7043C20.8108 20.707 20.6408 20.7549 20.4912 20.8433C20.3416 20.9317 20.2176 21.0576 20.1315 21.2086C20.0453 21.3595 20 21.5302 20 21.704V28.304C20 28.4778 20.0453 28.6486 20.1315 28.7995C20.2176 28.9504 20.3416 29.0763 20.4912 29.1647C20.6408 29.2531 20.8108 29.301 20.9846 29.3037C21.1583 29.3064 21.3298 29.2638 21.482 29.18L27.47 25.88C27.6268 25.7937 27.7575 25.6669 27.8486 25.5128C27.9397 25.3587 27.9877 25.183 27.9877 25.004C27.9877 24.825 27.9397 24.6493 27.8486 24.4952C27.7575 24.3412 27.6268 24.2143 27.47 24.128Z"
fill="white"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{audio?.title}
</div>
</div>
</div>
</Link>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
)}
</div>
);
};
export default AudioSliderPage;

View File

@ -0,0 +1,811 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
Music,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import WavesurferPlayer from "@wavesurfer/react";
import WaveSurfer from "wavesurfer.js";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { htmlToString } from "@/utils/globals";
import { getCookiesDecrypt } from "@/lib/utils";
import { loading } from "@/lib/swal";
import { formatDate } from "@fullcalendar/core/index.js";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
description: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailAudio() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
// const [detail, setDetail] = useState({
// title: null,
// tags: null,
// files: [],
// fileType: null,
// });
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [detailAudio, setDetailAudio] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false);
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
const onReady = (ws: any) => {
setWavesurfer(ws);
setIsPlaying(false);
};
const onPlayPause = () => {
wavesurfer && wavesurfer.playPause();
};
const addReply = (commentId: number) => {
if (replyText.trim()) {
const newCommentData = commentsData.map((comment: any) => {
if (comment.id === commentId) {
return {
...comment,
replies: [
...comment.replies,
{
text: replyText,
username: "You",
date: new Date().toLocaleString(),
},
],
};
}
return comment;
});
setCommentsData(newCommentData);
setReplyText("");
setReplyingTo(null);
}
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
secondaryUrl: file.secondaryUrl || "default-image.jpg",
placements: file.placements || "",
}));
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
setDetailAudio(fileUrls);
const fileUrlsThumbnail = filesData.map(
(file: { thumbnailFileUrl: string; placements: string }) => ({
thumbnailFileUrl: file.thumbnailFileUrl
? file.thumbnailFileUrl
: "default-image.jpg",
placements: file.placements || "",
})
);
setDetailThumb(fileUrlsThumbnail);
// setDetailThumb(fileUrls);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<CardContent className="border rounded-md">
<div className="flex flex-col sm:flex-row lg:flex-row lg:gap-10">
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.title}
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full">
<Label>Kategori</Label>
<Controller
control={control}
name="categoryName"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.categoryName}
/>
)}
/>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea
value={htmlToString(detail.description)}
onChange={field.onChange}
placeholder="Enter Meta"
cols={5}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label>Jenis Penugasan</Label>
<RadioGroup
value={selectedValue} // Set selected value
onValueChange={handleValueChange} // Update state on change
className="flex flex-wrap gap-3"
>
{/* Static list of radio buttons */}
<div className="flex items-center gap-2">
<RadioGroupItem value="5" id="umum" />
<Label htmlFor="umum">Umum</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="8" id="ksp" />
<Label htmlFor="ksp">Ksp</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="6" id="journalist" />
<Label htmlFor="journalist">Journalist</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="7" id="polri" />
<Label htmlFor="polri">Polri</Label>
</div>
</RadioGroup>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>Tag</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags?.split(",").map((tag, index) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>
{" "}
{detail?.uploadedBy?.fullname ||
"Data tidak tersedia"}
</Label>
</div>
</div>
<div className=" py-3">
<div className="flex flex-row items-center gap-2 text-blue-500">
<InboxIcon />
<p className="text-blue-500">Kotak Saran (0)</p>
</div>
</div>
<Button>Content Rewrite</Button>
</div>
</div>
</div>
</CardContent>
<CardContent className="border rounded-md mt-5">
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full lg:w-6/12 border px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label className="text-xl text-black">File Media</Label>
<div className="w-full">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailAudio?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<WavesurferPlayer
height={500}
waveColor="red"
url={data.secondaryUrl}
onReady={onReady}
key={`File ID: ${data.id}`}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
</SwiperSlide>
))}
</Swiper>
<Button
size="sm"
type="button"
onClick={onPlayPause}
disabled={isPlaying}
className={`flex items-center gap-2 ${
isPlaying
? "bg-gray-300 cursor-not-allowed"
: "bg-primary text-white"
} p-2 rounded`}
>
{isPlaying ? "Pause" : "Play"}
<Icon
icon={
isPlaying
? "carbon:pause-outline"
: "famicons:play-sharp"
}
className="h-5 w-5"
/>
</Button>
<div className=" mt-2 ">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{ clickable: true }}
modules={[Pagination, Thumbs]}
>
{detailAudio?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={"/assets/music-icon.jpg"}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12 border px-3 rounded-md mt-3">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">
Penempatan File
</Label>
<div className="flex flex-row justify-between items-center">
<p>file</p>
<p>Penempatan</p>
</div>
<p className="bg-black h-1 w-full rounded-lg"></p>
{detailThumb?.map((data: any) => (
<div
key={data.id}
className="flex items-center gap-3 mt-2"
>
<Music className="object-cover w-32 h-32" />
<div className="flex flex-wrap lg:flex-row gap-3 items-center">
{/* Mabes Checkbox */}
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "mabes"}
disabled
/>
<span>Nasional</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "polda"}
disabled
/>
<span>Wilayah</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "satker"}
disabled
/>
<span>Satker</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "international"}
disabled
/>
<span>International</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</CardContent>
<CardContent>
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Berikan Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -0,0 +1,120 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
import TeksAll from "./teks";
const DocumentAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Teks</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Teks</Label>
</div>
<div className="px-5 my-5">
<TeksAll />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="giat-penugasan">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<Label>Audio Visual</Label>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<Label>Audio</Label>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<Label>Foto</Label>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<Label>Teks</Label>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="contest">
<Card>
<div className="py-3">
<ContestTable />
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default DocumentAllPage;

View File

@ -0,0 +1,116 @@
"use client";
import { Link } from "@/components/navigation";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const TeksAll = () => {
const [documentData, setDocumentData] = useState<any>();
const [displayDocument, setDisplayDocument] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState("");
useEffect(() => {
initFetch();
}, [page, limit, search]);
useEffect(() => {
if (documentData?.length > 0) {
shuffleAndSetVideos();
const interval = setInterval(shuffleAndSetVideos, 5000);
return () => clearInterval(interval); // Cleanup interval on unmount
}
}, [documentData]);
const initFetch = async () => {
const response = await listCuratedContent(search, limit, page - 1, 3, "1");
console.log(response);
const data = response?.data?.data;
const contentData = data?.content;
setDocumentData(contentData);
};
const shuffleAndSetVideos = () => {
const shuffled = shuffleArray([...documentData]);
setDisplayDocument(shuffled.slice(0, 2));
};
const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
};
return (
<div className="mx-3 px-5">
<div className=" grid grid-cols-1 md:grid-cols-2 gap-6">
{displayDocument?.map((document: any) => (
<Link
href={`/shared/curated-content/giat-routine/document/detail/${document.id}`}
key={document?.id}
className="flex flex-col bg-yellow-500 sm:flex-row items-center dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full"
>
<div className="flex items-center justify-center rounded-lg w-16 h-16">
<svg
width="28"
height="34"
viewBox="0 0 28 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.6665 17.4167C5.6665 17.0851 5.7982 16.7672 6.03262 16.5328C6.26704 16.2984 6.58498 16.1667 6.9165 16.1667C7.24802 16.1667 7.56597 16.2984 7.80039 16.5328C8.03481 16.7672 8.1665 17.0851 8.1665 17.4167C8.1665 17.7482 8.03481 18.0661 7.80039 18.3005C7.56597 18.535 7.24802 18.6667 6.9165 18.6667C6.58498 18.6667 6.26704 18.535 6.03262 18.3005C5.7982 18.0661 5.6665 17.7482 5.6665 17.4167ZM6.9165 21.1667C6.58498 21.1667 6.26704 21.2984 6.03262 21.5328C5.7982 21.7672 5.6665 22.0851 5.6665 22.4167C5.6665 22.7482 5.7982 23.0661 6.03262 23.3005C6.26704 23.535 6.58498 23.6667 6.9165 23.6667C7.24802 23.6667 7.56597 23.535 7.80039 23.3005C8.03481 23.0661 8.1665 22.7482 8.1665 22.4167C8.1665 22.0851 8.03481 21.7672 7.80039 21.5328C7.56597 21.2984 7.24802 21.1667 6.9165 21.1667ZM5.6665 27.4167C5.6665 27.0851 5.7982 26.7672 6.03262 26.5328C6.26704 26.2984 6.58498 26.1667 6.9165 26.1667C7.24802 26.1667 7.56597 26.2984 7.80039 26.5328C8.03481 26.7672 8.1665 27.0851 8.1665 27.4167C8.1665 27.7482 8.03481 28.0661 7.80039 28.3005C7.56597 28.535 7.24802 28.6667 6.9165 28.6667C6.58498 28.6667 6.26704 28.535 6.03262 28.3005C5.7982 28.0661 5.6665 27.7482 5.6665 27.4167ZM11.9165 16.1667C11.585 16.1667 11.267 16.2984 11.0326 16.5328C10.7982 16.7672 10.6665 17.0851 10.6665 17.4167C10.6665 17.7482 10.7982 18.0661 11.0326 18.3005C11.267 18.535 11.585 18.6667 11.9165 18.6667H21.0832C21.4147 18.6667 21.7326 18.535 21.9671 18.3005C22.2015 18.0661 22.3332 17.7482 22.3332 17.4167C22.3332 17.0851 22.2015 16.7672 21.9671 16.5328C21.7326 16.2984 21.4147 16.1667 21.0832 16.1667H11.9165ZM10.6665 22.4167C10.6665 22.0851 10.7982 21.7672 11.0326 21.5328C11.267 21.2984 11.585 21.1667 11.9165 21.1667H21.0832C21.4147 21.1667 21.7326 21.2984 21.9671 21.5328C22.2015 21.7672 22.3332 22.0851 22.3332 22.4167C22.3332 22.7482 22.2015 23.0661 21.9671 23.3005C21.7326 23.535 21.4147 23.6667 21.0832 23.6667H11.9165C11.585 23.6667 11.267 23.535 11.0326 23.3005C10.7982 23.0661 10.6665 22.7482 10.6665 22.4167ZM11.9165 26.1667C11.585 26.1667 11.267 26.2984 11.0326 26.5328C10.7982 26.7672 10.6665 27.0851 10.6665 27.4167C10.6665 27.7482 10.7982 28.0661 11.0326 28.3005C11.267 28.535 11.585 28.6667 11.9165 28.6667H21.0832C21.4147 28.6667 21.7326 28.535 21.9671 28.3005C22.2015 28.0661 22.3332 27.7482 22.3332 27.4167C22.3332 27.0851 22.2015 26.7672 21.9671 26.5328C21.7326 26.2984 21.4147 26.1667 21.0832 26.1667H11.9165ZM26.3565 11.0233L16.6415 1.31C16.6157 1.28605 16.5885 1.26378 16.5598 1.24333C16.5392 1.22742 16.5192 1.21074 16.4998 1.19333C16.3852 1.08512 16.2632 0.984882 16.1348 0.893332C16.0922 0.865802 16.0476 0.841298 16.0015 0.819999L15.9215 0.779999L15.8382 0.731666C15.7482 0.679999 15.6565 0.626665 15.5615 0.586665C15.2296 0.454104 14.8783 0.376423 14.5215 0.356665C14.4885 0.354519 14.4557 0.350625 14.4232 0.344999C14.3779 0.338012 14.3323 0.334114 14.2865 0.333332H3.99984C3.11578 0.333332 2.26794 0.684521 1.64281 1.30964C1.01769 1.93476 0.666504 2.78261 0.666504 3.66667V30.3333C0.666504 31.2174 1.01769 32.0652 1.64281 32.6904C2.26794 33.3155 3.11578 33.6667 3.99984 33.6667H23.9998C24.8839 33.6667 25.7317 33.3155 26.3569 32.6904C26.982 32.0652 27.3332 31.2174 27.3332 30.3333V13.38C27.333 12.496 26.9817 11.6483 26.3565 11.0233ZM24.8332 30.3333C24.8332 30.5543 24.7454 30.7663 24.5891 30.9226C24.4328 31.0789 24.2208 31.1667 23.9998 31.1667H3.99984C3.77882 31.1667 3.56686 31.0789 3.41058 30.9226C3.2543 30.7663 3.1665 30.5543 3.1665 30.3333V3.66667C3.1665 3.44565 3.2543 3.23369 3.41058 3.07741C3.56686 2.92113 3.77882 2.83333 3.99984 2.83333H13.9998V10.3333C13.9998 11.2174 14.351 12.0652 14.9761 12.6904C15.6013 13.3155 16.4491 13.6667 17.3332 13.6667H24.8332V30.3333ZM16.4998 4.70166L22.9632 11.1667H17.3332C17.1122 11.1667 16.9002 11.0789 16.7439 10.9226C16.5876 10.7663 16.4998 10.5543 16.4998 10.3333V4.70166Z"
fill="black"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm">
{formatDateToIndonesian(new Date(document?.createdAt))}{" "}
{document?.timezone ? document?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" /> 518
</div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{document?.title}
</div>
<div className="flex gap-2 items-center text-sm text-red-500 dark:text-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 512 512"
>
<path
fill="#f00"
d="M224 30v256h-64l96 128l96-128h-64V30zM32 434v48h448v-48z"
/>
</svg>
Download Dokumen
</div>
</div>
</Link>
))}
</div>
</div>
);
};
export default TeksAll;

View File

@ -0,0 +1,814 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { htmlToString } from "@/utils/globals";
import { loading } from "@/lib/swal";
import { formatDate } from "@fullcalendar/core/index.js";
import { getCookiesDecrypt } from "@/lib/utils";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
type PublishedForObject = {
id: number;
name: string;
isInternal: boolean;
code: string;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
description: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailDocument() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
// const [detail, setDetail] = useState({
// title: null,
// tags: null,
// files: [],
// fileType: null,
// });
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
const addReply = (commentId: number) => {
if (replyText.trim()) {
const newCommentData = commentsData.map((comment: any) => {
if (comment.id === commentId) {
return {
...comment,
replies: [
...comment.replies,
{
text: replyText,
username: "You",
date: new Date().toLocaleString(),
},
],
};
}
return comment;
});
setCommentsData(newCommentData);
setReplyText("");
setReplyingTo(null);
}
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
url: file.secondaryUrl || "default-image.jpg",
format: file.format,
fileName: file.fileName,
placements: file.placements || "",
}));
setDetailThumb(fileUrls);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<CardContent className="border rounded-md">
<div className="flex flex-col sm:flex-row lg:flex-row lg:gap-10">
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.title}
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full">
<Label>Kategori</Label>
<Controller
control={control}
name="categoryName"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.categoryName}
/>
)}
/>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea
value={htmlToString(detail.description)}
onChange={field.onChange}
placeholder="Enter Meta"
cols={5}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</div>
</div>
</div>
<div className="w-6/12">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label>Jenis Penugasan</Label>
<RadioGroup
value={selectedValue} // Set selected value
onValueChange={handleValueChange} // Update state on change
className="flex flex-wrap gap-3"
>
{/* Static list of radio buttons */}
<div className="flex items-center gap-2">
<RadioGroupItem value="5" id="umum" />
<Label htmlFor="umum">Umum</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="8" id="ksp" />
<Label htmlFor="ksp">Ksp</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="6" id="journalist" />
<Label htmlFor="journalist">Journalist</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="7" id="polri" />
<Label htmlFor="polri">Polri</Label>
</div>
</RadioGroup>
</div>
<div className="py-3">
<div className="space-y-2">
<Label>Tag</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags?.split(",").map((tag, index) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>
{" "}
{detail?.uploadedBy?.fullname ||
"Data tidak tersedia"}
</Label>
</div>
</div>
<div className=" py-3">
<div className="flex flex-row items-center gap-2 text-blue-500">
<InboxIcon />
<p className="text-blue-500">Kotak Saran (0)</p>
</div>
</div>
<Button>Content Rewrite</Button>
</div>
</div>
</div>
</CardContent>
<CardContent className="border rounded-md mt-5">
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full lg:w-6/12 border px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label className="text-xl text-black">File Media</Label>
<div className="w-full ">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
{[".jpg", ".jpeg", ".png", ".webp"].includes(
data.format
) ? (
// Menampilkan gambar
<img
className="object-fill h-full w-full rounded-md"
src={data.url}
alt={data.fileName || "File"}
/>
) : data.format === ".pdf" ? (
// Menampilkan PDF menggunakan iframe
<iframe
className="w-full h-96 rounded-md"
src={data.url}
title={data.fileName || "PDF File"}
/>
) : [".docx", ".ppt", ".pptx"].includes(
data.format
) ? (
// Menampilkan file dokumen menggunakan Office Viewer
<iframe
className="w-full h-96 rounded-md"
src={`https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(
data.url
)}`}
title={data.fileName || "Document"}
/>
) : (
// Menampilkan link jika format tidak dikenali
<a
href={data.url}
target="_blank"
rel="noopener noreferrer"
className="block text-blue-500 underline"
>
View {data.fileName || "File"}
</a>
)}
</SwiperSlide>
))}
</Swiper>
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{ clickable: true }}
modules={[Pagination, Thumbs]}
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
{[".jpg", ".jpeg", ".png", ".webp"].includes(
data.format
) ? (
<img
className="object-cover h-[60px] w-[80px]"
src={"/assets/docx-icon.jpg"}
alt={data.fileName}
/>
) : (
<div className="h-[60px] w-[80px] flex items-center justify-center bg-gray-200 text-sm text-center text-gray-700 rounded-md">
{data?.format
?.replace(".", "")
.toUpperCase()}
</div>
)}
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12 border px-3 rounded-md mt-3">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">
Penempatan File
</Label>
<div className="flex flex-row justify-between items-center">
<p>file</p>
<p>Penempatan</p>
</div>
<p className="bg-black h-1 w-full rounded-lg"></p>
{detailThumb.map((data: any, index: number) => (
<div
key={index}
className="flex items-center gap-3 mt-2"
>
<img
className="object-cover h-[60px] w-[80px]"
src={"/assets/docx-icon.jpg"}
alt={data.fileName}
/>
<div className="flex flex-wrap lg:flex-row gap-3 items-center">
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "mabes"}
disabled
/>
<span>Nasional</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "polda"}
disabled
/>
<span>Wilayah</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "satker"}
disabled
/>
<span>Satker</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "international"}
disabled
/>
<span>International</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</CardContent>
<CardContent>
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Berikan Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -0,0 +1,125 @@
"use client";
import { Link } from "@/components/navigation";
import { Card } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { getListContent } from "@/service/landing/landing";
import {
formatDateToIndonesian,
generateLocalizedPath,
textEllipsis,
} from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useParams, usePathname, useRouter } from "next/navigation";
import React, { Component, useEffect, useState } from "react";
const TeksSliderPage = () => {
const [documentData, setDocumentData] = useState<any>();
const [displayDocument, setDisplayDocument] = useState<any[]>([]);
const [page, setPage] = useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState("");
const [hasData, setHasData] = useState(false);
useEffect(() => {
initFetch();
}, [page, limit, search]);
const initFetch = async () => {
const response = await listCuratedContent(search, limit, page - 1, 3, "1");
console.log(response);
const data = response?.data?.data;
const contentData = data?.content;
setHasData(displayDocument && displayDocument.length > 0);
setDisplayDocument(contentData);
};
return (
<div className="w-full px-2">
{displayDocument.length > 0 && (
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-semibold">Teks</h2>
<Link
href={"/shared/curated-content/giat-routine/video/all"}
className="text-sm text-gray-500 hover:text-gray-700 flex items-center"
>
Lihat Semua <Icon icon="lucide:arrow-right" className="ml-1" />
</Link>
</div>
<Carousel className="w-full">
<CarouselContent>
{displayDocument.map((document, index) => (
<CarouselItem key={index} className="md:basis-1/3 lg:basis-1/3">
<div className="p-2">
<Card className=" shadow-md rounded-lg overflow-hidden">
<Link
href={`/shared/curated-content/giat-routine/document/detail/${document.id}`}
key={document?.id}
className="flex flex-col bg-yellow-500 sm:flex-row items-center dark:bg-gray-800 cursor-pointer shadow-md rounded-lg p-4 gap-4 w-full"
>
<div className="flex items-center justify-center rounded-lg w-16 h-16">
<svg
width="28"
height="34"
viewBox="0 0 28 34"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.6665 17.4167C5.6665 17.0851 5.7982 16.7672 6.03262 16.5328C6.26704 16.2984 6.58498 16.1667 6.9165 16.1667C7.24802 16.1667 7.56597 16.2984 7.80039 16.5328C8.03481 16.7672 8.1665 17.0851 8.1665 17.4167C8.1665 17.7482 8.03481 18.0661 7.80039 18.3005C7.56597 18.535 7.24802 18.6667 6.9165 18.6667C6.58498 18.6667 6.26704 18.535 6.03262 18.3005C5.7982 18.0661 5.6665 17.7482 5.6665 17.4167ZM6.9165 21.1667C6.58498 21.1667 6.26704 21.2984 6.03262 21.5328C5.7982 21.7672 5.6665 22.0851 5.6665 22.4167C5.6665 22.7482 5.7982 23.0661 6.03262 23.3005C6.26704 23.535 6.58498 23.6667 6.9165 23.6667C7.24802 23.6667 7.56597 23.535 7.80039 23.3005C8.03481 23.0661 8.1665 22.7482 8.1665 22.4167C8.1665 22.0851 8.03481 21.7672 7.80039 21.5328C7.56597 21.2984 7.24802 21.1667 6.9165 21.1667ZM5.6665 27.4167C5.6665 27.0851 5.7982 26.7672 6.03262 26.5328C6.26704 26.2984 6.58498 26.1667 6.9165 26.1667C7.24802 26.1667 7.56597 26.2984 7.80039 26.5328C8.03481 26.7672 8.1665 27.0851 8.1665 27.4167C8.1665 27.7482 8.03481 28.0661 7.80039 28.3005C7.56597 28.535 7.24802 28.6667 6.9165 28.6667C6.58498 28.6667 6.26704 28.535 6.03262 28.3005C5.7982 28.0661 5.6665 27.7482 5.6665 27.4167ZM11.9165 16.1667C11.585 16.1667 11.267 16.2984 11.0326 16.5328C10.7982 16.7672 10.6665 17.0851 10.6665 17.4167C10.6665 17.7482 10.7982 18.0661 11.0326 18.3005C11.267 18.535 11.585 18.6667 11.9165 18.6667H21.0832C21.4147 18.6667 21.7326 18.535 21.9671 18.3005C22.2015 18.0661 22.3332 17.7482 22.3332 17.4167C22.3332 17.0851 22.2015 16.7672 21.9671 16.5328C21.7326 16.2984 21.4147 16.1667 21.0832 16.1667H11.9165ZM10.6665 22.4167C10.6665 22.0851 10.7982 21.7672 11.0326 21.5328C11.267 21.2984 11.585 21.1667 11.9165 21.1667H21.0832C21.4147 21.1667 21.7326 21.2984 21.9671 21.5328C22.2015 21.7672 22.3332 22.0851 22.3332 22.4167C22.3332 22.7482 22.2015 23.0661 21.9671 23.3005C21.7326 23.535 21.4147 23.6667 21.0832 23.6667H11.9165C11.585 23.6667 11.267 23.535 11.0326 23.3005C10.7982 23.0661 10.6665 22.7482 10.6665 22.4167ZM11.9165 26.1667C11.585 26.1667 11.267 26.2984 11.0326 26.5328C10.7982 26.7672 10.6665 27.0851 10.6665 27.4167C10.6665 27.7482 10.7982 28.0661 11.0326 28.3005C11.267 28.535 11.585 28.6667 11.9165 28.6667H21.0832C21.4147 28.6667 21.7326 28.535 21.9671 28.3005C22.2015 28.0661 22.3332 27.7482 22.3332 27.4167C22.3332 27.0851 22.2015 26.7672 21.9671 26.5328C21.7326 26.2984 21.4147 26.1667 21.0832 26.1667H11.9165ZM26.3565 11.0233L16.6415 1.31C16.6157 1.28605 16.5885 1.26378 16.5598 1.24333C16.5392 1.22742 16.5192 1.21074 16.4998 1.19333C16.3852 1.08512 16.2632 0.984882 16.1348 0.893332C16.0922 0.865802 16.0476 0.841298 16.0015 0.819999L15.9215 0.779999L15.8382 0.731666C15.7482 0.679999 15.6565 0.626665 15.5615 0.586665C15.2296 0.454104 14.8783 0.376423 14.5215 0.356665C14.4885 0.354519 14.4557 0.350625 14.4232 0.344999C14.3779 0.338012 14.3323 0.334114 14.2865 0.333332H3.99984C3.11578 0.333332 2.26794 0.684521 1.64281 1.30964C1.01769 1.93476 0.666504 2.78261 0.666504 3.66667V30.3333C0.666504 31.2174 1.01769 32.0652 1.64281 32.6904C2.26794 33.3155 3.11578 33.6667 3.99984 33.6667H23.9998C24.8839 33.6667 25.7317 33.3155 26.3569 32.6904C26.982 32.0652 27.3332 31.2174 27.3332 30.3333V13.38C27.333 12.496 26.9817 11.6483 26.3565 11.0233ZM24.8332 30.3333C24.8332 30.5543 24.7454 30.7663 24.5891 30.9226C24.4328 31.0789 24.2208 31.1667 23.9998 31.1667H3.99984C3.77882 31.1667 3.56686 31.0789 3.41058 30.9226C3.2543 30.7663 3.1665 30.5543 3.1665 30.3333V3.66667C3.1665 3.44565 3.2543 3.23369 3.41058 3.07741C3.56686 2.92113 3.77882 2.83333 3.99984 2.83333H13.9998V10.3333C13.9998 11.2174 14.351 12.0652 14.9761 12.6904C15.6013 13.3155 16.4491 13.6667 17.3332 13.6667H24.8332V30.3333ZM16.4998 4.70166L22.9632 11.1667H17.3332C17.1122 11.1667 16.9002 11.0789 16.7439 10.9226C16.5876 10.7663 16.4998 10.5543 16.4998 10.3333V4.70166Z"
fill="black"
/>
</svg>
</div>
<div className="flex flex-col flex-1">
<div className="text-gray-500 dark:text-gray-400 flex flex-row text-sm">
{formatDateToIndonesian(
new Date(document?.createdAt)
)}{" "}
{document?.timezone ? document?.timezone : "WIB"} |{" "}
<Icon icon="formkit:eye" width="15" height="15" />{" "}
518
</div>
<div className="font-semibold text-gray-900 dark:text-white mt-1 text-sm">
{document?.title}
</div>
<div className="flex gap-2 items-center text-sm text-red-500 dark:text-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 512 512"
>
<path
fill="#f00"
d="M224 30v256h-64l96 128l96-128h-64V30zM32 434v48h448v-48z"
/>
</svg>
Download Dokumen
</div>
</div>
</Link>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
)}
</div>
);
};
export default TeksSliderPage;

View File

@ -0,0 +1,19 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTaskTa from "@/components/form/task-ta/task-ta-form";
import FormAskExpert from "@/components/form/shared/ask-expert-form";
import FormDoItYourself from "@/components/form/shared/do-it-yourself-form";
import FormAcceptAssignment from "@/components/form/shared/accept-assignment-form";
const AcceptAssignmentPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormAcceptAssignment />
</div>
</div>
);
};
export default AcceptAssignmentPage;

View File

@ -0,0 +1,619 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { listData } from "@/service/landing/landing";
import {
createAssignmentResponse,
deleteAssignmentResponse,
getAssignmentResponseList,
} from "@/service/task";
import { getCookiesDecrypt } from "@/lib/utils";
import { close, loading } from "@/lib/swal";
import { Checkbox } from "@/components/ui/checkbox";
import { formatDateToIndonesian, htmlToString } from "@/utils/globals";
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { Icon } from "@/components/ui/icon";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
// Pastikan validitas tanggal
if (isNaN(date.getTime())) {
throw new Error("Invalid date format");
}
// Format tanggal
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
// const hours = date.getHours().toString().padStart(2, "0");
// Gabungkan hasil format
return `${day}-${month}-${year} `;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
htmlDescription: string;
updatedAt: string;
timezone: string;
clickCount: string;
creatorName: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailAcceptImage() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const t = useTranslations("LandingPage");
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [showInput, setShowInput] = useState<boolean>(false);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
// const postData = () => {
// sendSuggestionParent();
// };
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
thumbnailFileUrl: file.thumbnailFileUrl || "default-image.jpg",
placements: file.placements || "",
}));
setDetailThumb(fileUrls);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<div className="w-full ">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
<div className=" mt-2 ">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{
clickable: true,
}}
modules={[Pagination, Thumbs]}
// className="mySwiper2"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
<div className="text-gray-500 flex flex-col lg:flex-row justify-between items-center border-t mt-4">
<div className="flex flex-col lg:flex-row items-center mt-3 lg:justify-between">
<p className="text-xs lg:text-sm">
{t("by", { defaultValue: "By" })}&nbsp;
<span className="font-semibold text-black dark:text-white">
{detail?.uploadedBy?.userLevel?.name}
</span>
</p>
{/* <p className="text-xs lg:text-sm">
&nbsp;|&nbsp;{t("updatedOn", { defaultValue: "Updated On" })}
{detail?.updatedAt} WIB &nbsp;|&nbsp;
</p> */}
<p className="text-xs lg:text-sm">
&nbsp;|&nbsp;{t("updatedOn", { defaultValue: "Updated On" })}&nbsp;
{formatDateToIndonesian(new Date(detail?.updatedAt))}{" "}
{detail?.timezone ? detail?.timezone : "WIB"}
&nbsp;
</p>
<p className="text-xs lg:text-sm flex justify-center items-center">
&nbsp;|&nbsp;
<Icon icon="formkit:eye" width="15" height="15" />
&nbsp; {detail?.clickCount} &nbsp;
</p>
</div>
<div className="mt-3">
<p className="flex text-end text-xs lg:text-sm font-semibold">
{t("creator", { defaultValue: "Creator" })}
{detail?.creatorName}
</p>
</div>
</div>
{/* Keterangan */}
<div className="w-full">
<h1 className="flex flex-row font-bold text-lg lg:text-2xl my-8">
{detail?.title}
</h1>
<div
className="font-light text-justify mb-5 space-y-4 lg:mb-0"
dangerouslySetInnerHTML={{
__html: detail?.htmlDescription,
}}
/>
</div>
</div>
</div>
<CardContent className="p-1">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -0,0 +1,70 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../../video/audio-visual";
const ImageAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Image</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3"></div>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default ImageAllPage;

View File

@ -0,0 +1,17 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTaskTa from "@/components/form/task-ta/task-ta-form";
import FormAskExpert from "@/components/form/shared/ask-expert-form";
const AskExpertCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormAskExpert />
</div>
</div>
);
};
export default AskExpertCreatePage;

View File

@ -0,0 +1,772 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { listData } from "@/service/landing/landing";
import {
createAssignmentResponse,
deleteAssignmentResponse,
getAssignmentResponseList,
} from "@/service/task";
import { getCookiesDecrypt } from "@/lib/utils";
import { close, loading } from "@/lib/swal";
import { Checkbox } from "@/components/ui/checkbox";
import { htmlToString } from "@/utils/globals";
import { Link } from "@/i18n/routing";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
// Pastikan validitas tanggal
if (isNaN(date.getTime())) {
throw new Error("Invalid date format");
}
// Format tanggal
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
// const hours = date.getHours().toString().padStart(2, "0");
// Gabungkan hasil format
return `${day}-${month}-${year} `;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
description: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailImage() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [showInput, setShowInput] = useState<boolean>(false);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
// const postData = () => {
// sendSuggestionParent();
// };
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
thumbnailFileUrl: file.thumbnailFileUrl || "default-image.jpg",
placements: file.placements || "",
}));
setDetailThumb(fileUrls);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<CardContent className="border rounded-md">
<div className="flex flex-col sm:flex-row lg:flex-row lg:gap-10">
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.title}
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full">
<Label>Kategori</Label>
<Controller
control={control}
name="categoryName"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.categoryName}
/>
)}
/>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea
value={htmlToString(detail.description)}
onChange={field.onChange}
placeholder="Enter Meta"
cols={5}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label>Jenis Penugasan</Label>
<RadioGroup
value={selectedValue} // Set selected value
onValueChange={handleValueChange} // Update state on change
className="flex flex-wrap gap-3"
>
{/* Static list of radio buttons */}
<div className="flex items-center gap-2">
<RadioGroupItem value="5" id="umum" />
<Label htmlFor="umum">Umum</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="8" id="ksp" />
<Label htmlFor="ksp">Ksp</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="6" id="journalist" />
<Label htmlFor="journalist">Journalist</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="7" id="polri" />
<Label htmlFor="polri">Polri</Label>
</div>
</RadioGroup>
</div>
<div className="py-3">
<div className="space-y-2">
<Label>Tag</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags?.split(",").map((tag, index) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>
{" "}
{detail?.uploadedBy?.fullname ||
"Data tidak tersedia"}
</Label>
</div>
</div>
<div className=" py-3">
<div className="flex flex-row items-center gap-2 text-blue-500">
<InboxIcon />
<p className="text-blue-500">Kotak Saran (0)</p>
</div>
</div>
<Link
href={
"/shared/curated-content/giat-routine/image/detail/content-rewrite"
}
>
<Button>Content Rewrite</Button>
</Link>
</div>
</div>
</div>
</CardContent>
<CardContent className="border rounded-md mt-5">
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full lg:w-6/12 border px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label className="text-xl text-black">File Media</Label>
<div className="w-full ">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
<div className=" mt-2 ">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{
clickable: true,
}}
modules={[Pagination, Thumbs]}
// className="mySwiper2"
>
{detailThumb?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<img
className="object-fill h-full w-full rounded-md"
src={data.thumbnailFileUrl}
alt={`File ID: ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12 border px-3 rounded-md mt-3">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">
Penempatan File
</Label>
<div className="flex flex-row justify-between items-center">
<p>file</p>
<p>Penempatan</p>
</div>
<p className="bg-black h-1 w-full rounded-lg"></p>
{detailThumb?.map((data: any) => (
<div
key={data.id}
className="flex items-center gap-3 mt-2"
>
{/* <img
className="object-cover w-20 h-20"
src={data.thumbnailUrl} // Assuming `thumbnailUrl` is the property that contains the URL for the thumbnail image
alt={`Thumbnail ${index}`}
/> */}
<img
className="object-cover h-20 w-20 lg:w-36 lg:h-32 rounded-md"
src={data.thumbnailFileUrl}
alt={`Article ${data.id}`}
/>
<div className="flex flex-wrap lg:flex-row gap-3 items-center">
{/* Mabes Checkbox */}
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "mabes"}
disabled
/>
<span>Nasional</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "polda"}
disabled
/>
<span>Wilayah</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "satker"}
disabled
/>
<span>Satker</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "international"}
disabled
/>
<span>International</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</CardContent>
<CardContent>
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -0,0 +1,349 @@
"use client";
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Controller, useForm } from "react-hook-form";
import CustomEditor from "@/components/editor/custom-editor";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Checkbox } from "@/components/ui/checkbox";
import { Link } from "@/i18n/routing";
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
const ContentRewritePage = () => {
const [step, setStep] = useState("configuration");
const [selectedLanguage, setSelectedLanguage] = useState("");
const [selectedMainKeyword, setSelectedMainKeyword] = useState("");
const [selectedWritingStyle, setSelectedWritingStyle] = useState("");
const [selectedSize, setSelectedSize] = useState("");
const [selectedSort, setSelectedSort] = useState("");
const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
null
);
type ImageSchema = z.infer<typeof imageSchema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
return (
<div className="p-6">
<Card>
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-4">Content Rewrite</h2>
<div className="flex items-center space-x-6 mb-6">
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 flex items-center justify-center border-2 rounded-full ${
step === "configuration"
? "border-blue-500 text-blue-500"
: "border-gray-400 text-gray-400"
}`}
>
</div>
<p
className={
step === "configuration"
? "text-blue-500 font-semibold"
: "text-gray-400"
}
>
Configuration
</p>
</div>
<div
className={`flex-1 h-0.5 ${
step === "draft"
? "bg-blue-500 text-blue-500"
: "bg-gray-400 text-gray-400"
}`}
></div>
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 flex items-center justify-center border-2 rounded-full ${
step === "draft"
? "border-blue-500 text-blue-500"
: "border-gray-400 text-gray-400"
}`}
>
</div>
<p
className={
step === "draft"
? "text-blue-500 font-semibold"
: "text-gray-400"
}
>
Draft
</p>
</div>
<div
className={`flex-1 h-0.5 ${
step === "publish"
? "bg-blue-500 text-blue-500"
: "bg-gray-400 text-gray-400"
}`}
></div>
<div className="flex flex-col items-center">
<div
className={`w-8 h-8 flex items-center justify-center border-2 rounded-full ${
step === "publish"
? "border-blue-500 text-blue-500"
: "border-gray-400 text-gray-400"
}`}
>
</div>
<p
className={
step === "publish"
? "text-blue-500 font-semibold"
: "text-gray-400"
}
>
Publish
</p>
</div>
</div>
{step === "configuration" && (
<>
<div className="flex flex-row gap-3">
<div className="space-y-2 py-3 w-4/12">
<Label>Bahasa</Label>
<Select onValueChange={setSelectedLanguage}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 py-3 w-4/12">
<Label>Writing Style</Label>
<Select onValueChange={setSelectedWritingStyle}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="friendly">Friendly</SelectItem>
<SelectItem value="profesional">Profesional</SelectItem>
<SelectItem value="informational">
Informational
</SelectItem>
<SelectItem value="neutral">Neutral</SelectItem>
<SelectItem value="witty">Witty</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 py-3 w-4/12">
<Label>Article Size</Label>
<Select onValueChange={setSelectedSize}>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="news">
News (300 - 900 words)
</SelectItem>
<SelectItem value="info">
Info (900 - 2000 words)
</SelectItem>
<SelectItem value="detail">
Detail (2000 - 5000 words)
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="mb-4">
<Label>Configuration</Label>
<Input
placeholder="Type your custom instruction here!"
className="h-20"
/>
</div>
<Button
onClick={() => setStep("draft")}
className=" bg-blue-600 text-white"
>
Selanjutnya
</Button>
</>
)}
{step === "draft" && (
<div>
<div className="flex flex-row justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="accepted"
// checked={filtered.includes("polri")}
// onCheckedChange={(e) => handleFilter("polri", Boolean(e))}
/>
<label
htmlFor="accepted"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Select All
</label>
</div>
<div className="space-y-2 py-3">
<div className="flex flex-row items-center">
<Label className="w-[50px]">Sort by</Label>
<Select onValueChange={setSelectedSort}>
<SelectTrigger size="sm" className="w-[150px]">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="id">Indonesia</SelectItem>
<SelectItem value="en">English</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{[
{
src: "/assets/img/image1.png",
alt: "Article 1",
title: "Kurang dari 24 Jam Polres Muara Enim Ungka...",
},
{
src: "/assets/img/image3.png",
alt: "Article 2",
title: "Kurang dari 24 Jam Polres Muara Enim Ungka...",
},
{
src: "/assets/img/image3.png",
alt: "Article 3",
title: "Polres Magelang Kota Konferensi Pers Terkait...",
},
].map((article, index) => (
<div
key={index}
className="border rounded-md overflow-hidden relative"
>
<input
type="checkbox"
className="absolute top-2 left-2 w-5 h-5 cursor-pointer"
/>
<img
src={article.src}
alt={article.alt}
className="w-full h-40 object-cover"
/>
<p className="p-2 text-sm">{article.title}</p>
</div>
))}
</div>
<div className="flex flex-row justify-between mt-3">
<Button
onClick={() => setStep("configuration")}
variant={"outline"}
color="primary"
>
Kembali
</Button>
<Button
onClick={() => setStep("publish")}
variant={"default"}
color="primary"
>
Selanjutnya
</Button>
</div>
</div>
)}
{step === "publish" && (
<div>
<div className="py-3">
<div className="flex flex-row justify-between items-center mb-3">
<Label>940 Words</Label>
<Link
href={`/contributor/content/image/update-seo/${selectedArticleId}`}
>
<Button
className="mb-2"
size="sm"
variant={"outline"}
color="primary"
>
Edit
</Button>
</Link>
</div>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={onChange}
// initialData={articleBody}
/>
)}
/>
{/* {errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)} */}
</div>
<div className="flex flex-row justify-between mt-3">
<Button
onClick={() => setStep("configuration")}
variant={"outline"}
color="primary"
>
Kembali
</Button>
<Button
onClick={() => setStep("publish")}
variant={"default"}
color="primary"
>
Selanjutnya
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
};
export default ContentRewritePage;

View File

@ -0,0 +1,18 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTaskTa from "@/components/form/task-ta/task-ta-form";
import FormAskExpert from "@/components/form/shared/ask-expert-form";
import FormDoItYourself from "@/components/form/shared/do-it-yourself-form";
const DoItYourselfCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormDoItYourself />
</div>
</div>
);
};
export default DoItYourselfCreatePage;

View File

@ -0,0 +1,146 @@
"use client";
import { Link } from "@/components/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { getCookiesDecrypt } from "@/lib/utils";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { formatDateToIndonesian } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
type ImageData = {
id: string;
title: string;
createdAt: string;
timezone: string;
thumbnailLink: string;
clickCount: string;
};
const ImageSliderPage = () => {
const router = useRouter();
const roleId = Number(getCookiesDecrypt("urie")) || 0;
const [imageData, setImageData] = useState<ImageData[]>([]);
const [page, setPage] = useState(1);
const [limit] = useState(10);
useEffect(() => {
fetchData();
}, [page]);
const fetchData = async () => {
const response = await listCuratedContent("", limit, page - 1, 1, "1");
const data = response?.data?.data?.content || [];
setImageData(data);
};
return (
<div className="w-full px-2">
{imageData.length > 0 && (
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-semibold">Foto</h2>
<Link
href={"/shared/curated-content/giat-routine/image/all"}
className="text-sm text-gray-500 hover:text-gray-700 flex items-center"
>
Lihat Semua <Icon icon="lucide:arrow-right" className="ml-1" />
</Link>
</div>
<Carousel className="w-full">
<CarouselContent>
{imageData.map((image, index) => (
<CarouselItem key={index} className="md:basis-1/3 lg:basis-1/3">
<div className="p-2">
<Card className="shadow-md rounded-lg overflow-hidden">
<Link
href={
roleId === 12
? `/shared/curated-content/giat-routine/image/accept-assignment/detail/${image.id}`
: `/shared/curated-content/giat-routine/image/detail/${image.id}`
}
>
<CardContent className="p-0">
<img
src={image?.thumbnailLink}
alt={image?.title}
className="w-full h-56 object-cover rounded-t-lg"
/>
<div className="p-3">
<p className="text-xs text-gray-500">
{formatDateToIndonesian(
new Date(image?.createdAt)
)}{" "}
{image?.timezone || "WIB"} |
<Icon
icon="formkit:eye"
width="15"
height="15"
className="inline ml-1"
/>
{image?.clickCount}
</p>
<h3 className="font-semibold text-sm truncate">
{image?.title}
</h3>
</div>
</CardContent>
</Link>
{roleId === 11 && (
<div className="flex flex-row justify-between mx-3 mb-3">
<Link
href={`/shared/curated-content/giat-routine/image/ask-the-expert/${image.id}`}
>
<Button color="primary" size="md">
Ask The Expert
</Button>
</Link>
<Link
href={`/shared/curated-content/giat-routine/image/do-it-yourself/${image.id}`}
>
<Button color="primary" size="md">
Do it Yourself
</Button>
</Link>
</div>
)}
{roleId === 12 && (
<div className="mx-3 mb-3">
<Link
href={`/shared/curated-content/giat-routine/image/accept-assignment/${image.id}`}
>
<Button
className="w-full "
color="primary"
size="md"
>
Accept Assignment
</Button>
</Link>
</div>
)}
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
)}
</div>
);
};
export default ImageSliderPage;

View File

@ -0,0 +1,119 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import ContestTable from "../../../../contest/components/contest-table";
import AudioSliderPage from "../../audio/audio";
import TeksSliderPage from "../../document/teks";
import ImageSliderPage from "../../image/image";
import VideoSliderPage from "../audio-visual";
const VideoAllPage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4 h-20 flex items-center">
<p className="text-lg font-semibold ml-2">Konten Video</p>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="flex justify-between items-center mx-3">
<Label className="text-base">Audio Visual</Label>
</div>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="giat-penugasan">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<Label>Audio Visual</Label>
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<Label>Audio</Label>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<Label>Foto</Label>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<Label>Teks</Label>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="contest">
<Card>
<div className="py-3">
<ContestTable />
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default VideoAllPage;

View File

@ -0,0 +1,111 @@
"use client";
import { Link } from "@/components/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Label } from "@/components/ui/label";
import { listCuratedContent } from "@/service/curated-content/curated-content";
import { getListContent } from "@/service/landing/landing";
import { formatDateToIndonesian } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import image from "next/image";
import React, { useEffect, useState } from "react";
type VideoData = {
id: string;
title: string;
createdAt: string;
timezone: string;
thumbnailLink: string;
clickCount: string;
};
const VideoSliderPage = () => {
const [allVideoData, setAllVideoData] = useState<any[]>([]);
const [videoData, setVideoData] = useState<VideoData[]>([]);
const [page, setPage] = useState(1);
const [limit, setLimit] = React.useState(10);
const [search, setSearch] = React.useState("");
useEffect(() => {
fetchData();
}, [page, limit, search]);
const fetchData = async () => {
const response = await listCuratedContent(search, limit, page - 1, 2, "1");
console.log(response);
const data = response?.data?.data;
const contentData = data?.content;
setVideoData(contentData);
};
return (
<div className="w-full px-2">
{videoData.length > 0 && (
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-semibold">Video</h2>
<Link
href={"/shared/curated-content/giat-routine/video/all"}
className="text-sm text-gray-500 hover:text-gray-700 flex items-center"
>
Lihat Semua <Icon icon="lucide:arrow-right" className="ml-1" />
</Link>
</div>
<Carousel className="w-full">
<CarouselContent>
{videoData.map((video, index) => (
<CarouselItem key={index} className="md:basis-1/3 lg:basis-1/3">
<div className="p-2">
<Card className="shadow-md rounded-lg overflow-hidden">
<Link
href={`/shared/curated-content/giat-routine/video/detail/${video.id}`}
>
<CardContent className="p-0">
<img
src={video?.thumbnailLink}
alt={video?.title}
className="w-full h-56 object-cover rounded-t-lg"
/>
<div className="p-3">
<p className="text-xs text-gray-500">
{formatDateToIndonesian(
new Date(video?.createdAt)
)}{" "}
{video?.timezone || "WIB"} |
<Icon
icon="formkit:eye"
width="15"
height="15"
className="inline ml-1"
/>
{video?.clickCount}
</p>
<h3 className="font-semibold text-sm truncate">
{video?.title}
</h3>
</div>
</CardContent>
</Link>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
</div>
)}
</div>
);
};
export default VideoSliderPage;

View File

@ -0,0 +1,763 @@
"use client";
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card, CardContent } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import Cookies from "js-cookie";
import { postBlog } from "@/service/blog/blog";
import { Textarea } from "@/components/ui/textarea";
import {
DotSquare,
InboxIcon,
PaperclipIcon,
SmileIcon,
TrashIcon,
} from "lucide-react";
import {
deleteMediaCurationMessage,
detailMedia,
getMediaCurationMessage,
saveMediaCurationMessage,
} from "@/service/curated-content/curated-content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/free-mode";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import "swiper/css";
import "swiper/css/navigation";
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { getCookiesDecrypt } from "@/lib/utils";
import { formatDate } from "@fullcalendar/core/index.js";
import { loading } from "@/lib/swal";
import { htmlToString } from "@/utils/globals";
import { Checkbox } from "@/components/ui/checkbox";
const detailSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
categoryName: z.string().min(1, { message: "Judul diperlukan" }),
meta: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
categoryName: string;
};
export type curationDetail = {
id: number;
title: string;
categoryName: string;
description: string;
uploadedBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
publishedFor: string; // ID for selected radio button
publishedForObject: {
id: number;
name: string;
isInternal: boolean;
code: string;
}[];
tags: string;
provinceId: string;
is_active: string;
};
const initialComments = [
{
id: 1,
username: "Esther Howard",
date: "07-04-2023 20:00 WIB",
text: "Tolong untuk narasinya mengikuti 5W + 1H!",
avatar: "/images/avatar/avatar-3.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 2,
username: "Brooklyn Simmons",
date: "07-04-2023 20:00 WIB",
text: "Ok Baik, Saya segera melakukan perbaikan. Terima kasih atas masukannya. 🙏",
avatar: "/images/avatar/avatar-5.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
{
id: 3,
username: "Leslie Alexander",
date: "07-04-2023 20:00 WIB",
text: "Sangat berguna. Terima Kasih!",
avatar: "/images/avatar/avatar-7.png", // URL avatar atau path gambar pengguna
replies: [], // Komentar balasan
},
];
export default function DetailImage() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type DetailSchema = z.infer<typeof detailSchema>;
const userLevelNumber = getCookiesDecrypt("ulne");
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [selectedTarget, setSelectedTarget] = useState("");
// const [detail, setDetail] = useState({
// title: null,
// tags: null,
// files: [],
// fileType: null,
// });
const [detail, setDetail] = useState<curationDetail>();
const [refresh] = useState(false);
const [detailVideo, setDetailVideo] = useState<any>([]);
const [detailThumb, setDetailThumb] = useState<any>([]);
const [thumbsSwiper, setThumbsSwiper] = useState<any>(null);
const [selectedValue, setSelectedValue] = useState<string>("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<DetailSchema>({
resolver: zodResolver(detailSchema),
});
const [commentsData, setCommentsData] = useState(initialComments);
const [replyText, setReplyText] = useState("");
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [selectedFileId, setSelectedFileId] = useState(null);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState("");
const handleReply = (commentId: number) => {
setReplyingTo(commentId);
};
useEffect(() => {
async function initState() {
// loading();
const response = await getMediaCurationMessage(selectedFileId);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, [selectedFileId]);
const postData = async () => {
if (message?.length > 1 && selectedFileId) {
try {
const data = {
mediaUploadFileId: selectedFileId,
message,
parentId: null,
};
const response = await saveMediaCurationMessage(data);
console.log("Komentar terkirim:", response);
const responseGet = await getMediaCurationMessage(selectedFileId);
setListData(responseGet?.data?.data);
setMessage("");
} catch (error) {
console.error("Error posting comment:", error);
}
} else {
console.log("Pesan atau file ID tidak valid.");
}
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1 && selectedFileId) {
loading();
const data = {
mediaUploadFileId: selectedFileId,
message: inputElement.value,
parentId,
};
console.log("Sending reply:", data);
const response = await saveMediaCurationMessage(data);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log("Updated comments:", responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
// useEffect(() => {
// async function initState() {
// // loading();
// const response = await getMediaCurationMessage(selectedFileId);
// console.log("data", response?.data?.data);
// console.log("userLvl", userLevelNumber);
// setListData(response?.data?.data);
// close();
// }
// initState();
// }, [selectedFileId]);
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteMediaCurationMessage(dataId);
console.log(response);
const responseGet = await getMediaCurationMessage(selectedFileId);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
const filesData = details.files || [];
const fileUrls = filesData.map((file: any) => ({
id: file.id,
url: file.url || "default-image.jpg",
placements: file.placements || "",
}));
setDetail(details);
setSelectedValue(details?.publishedFor || "");
setSelectedFileId(details?.files[0]?.id);
setDetailVideo(fileUrls);
const filesDataThumbnail = details.files || [];
const fileUrlsThumbnail = filesData.map(
(file: { thumbnailFileUrl: string; placements: string }) => ({
thumbnailFileUrl: file.thumbnailFileUrl
? file.thumbnailFileUrl
: "default-image.jpg",
placements: file.placements || "",
})
);
setDetailThumb(fileUrlsThumbnail);
}
}
initState();
}, [id, refresh]);
const handleFileClick = async (fileId: any) => {
setSelectedFileId(fileId);
try {
const response = await getMediaCurationMessage(fileId);
console.log("Data komentar:", response?.data?.data);
setListData(response?.data?.data);
} catch (error) {
console.error("Error fetching comments:", error);
}
};
const handleValueChange = (value: string) => {
setSelectedValue(value);
};
return (
<div className="flex gap-10">
{detail !== undefined ? (
<Card className="w-full ">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Kurasi Detail</p>
<CardContent className="border rounded-md">
<div className="flex flex-col sm:flex-row lg:flex-row lg:gap-10">
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.title}
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)}
</div>
<div className="flex items-center">
<div className="py-3 w-full">
<Label>Kategori</Label>
<Controller
control={control}
name="categoryName"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
defaultValue={detail.categoryName}
/>
)}
/>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea
value={htmlToString(detail.description)}
onChange={field.onChange}
placeholder="Enter Meta"
cols={5}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label>Jenis Penugasan</Label>
<RadioGroup
value={selectedValue} // Set selected value
onValueChange={handleValueChange} // Update state on change
className="flex flex-wrap gap-3"
>
{/* Static list of radio buttons */}
<div className="flex items-center gap-2">
<RadioGroupItem value="5" id="umum" />
<Label htmlFor="umum">Umum</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="8" id="ksp" />
<Label htmlFor="ksp">Ksp</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="6" id="journalist" />
<Label htmlFor="journalist">Journalist</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="7" id="polri" />
<Label htmlFor="polri">Polri</Label>
</div>
</RadioGroup>
</div>
<div className="py-3">
<div className="space-y-2">
<Label>Tag</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags?.split(",").map((tag, index) => (
<Badge
key={index}
className="border rounded-md px-2 py-2"
>
{tag.trim()}
</Badge>
))}
</div>
</div>
</div>
<div className=" py-3">
<div className="space-y-2">
<Label>
{" "}
{detail?.uploadedBy?.fullname ||
"Data tidak tersedia"}
</Label>
</div>
</div>
<div className=" py-3">
<div className="flex flex-row items-center gap-2 text-blue-500">
<InboxIcon />
<p className="text-blue-500">Kotak Saran (0)</p>
</div>
</div>
<Button>Content Rewrite</Button>
</div>
</div>
</div>
</CardContent>
<CardContent className="border rounded-md mt-5">
<div className="flex flex-col lg:flex-row gap-5">
<div className="w-full lg:w-6/12 border px-3 mt-3 rounded-md">
<div className="gap-5 mb-5">
<div className="space-y-2 py-3">
<Label className="text-xl text-black">File Media</Label>
<div className="w-full">
<Swiper
thumbs={{ swiper: thumbsSwiper }}
modules={[FreeMode, Navigation, Thumbs]}
navigation={false}
className="w-full"
>
{detailVideo?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<video
className="object-fill h-full w-full rounded-md"
src={data.url}
controls
title={`Video ${data.id}`}
/>
</SwiperSlide>
))}
</Swiper>
<div className="mt-2">
<Swiper
onSwiper={setThumbsSwiper}
slidesPerView={6}
spaceBetween={8}
pagination={{
clickable: true,
}}
modules={[Pagination, Thumbs]}
>
{detailVideo?.map((data: any) => (
<SwiperSlide
key={data.id}
onClick={() => handleFileClick(data.id)}
>
<video
className="object-cover h-[60px] w-[80px] rounded-md"
src={data.url}
muted
title={`Video ${data.id}`} // Mengganti alt dengan title
/>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</div>
</div>
<div className="w-full lg:w-6/12 border px-3 rounded-md mt-3">
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">
Penempatan File
</Label>
<div className="flex flex-row justify-between items-center">
<p>file</p>
<p>Penempatan</p>
</div>
<p className="bg-black h-1 w-full rounded-lg"></p>
{detailThumb?.map((data: any) => (
<div
key={data.id}
className="flex items-center gap-3 mt-2"
>
<img
className="object-cover w-20 h-20 lg:w-32 lg:h-32"
src={"/assets/video-icon.webp"}
alt={` ${data.id}`}
/>
<div className="flex flex-wrap lg:flex-row gap-3 items-center">
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "mabes"}
disabled
/>
<span>Nasional</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "polda"}
disabled
/>
<span>Wilayah</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "satker"}
disabled
/>
<span>Satker</span>
</label>
<label className=" cursor-pointer flex items-center gap-2">
<Checkbox
checked={data.placements === "international"}
disabled
/>
<span>International</span>
</label>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</CardContent>
<CardContent>
<div className="gap-5 mb-5">
<div className="mt-5">
<Label className="text-xl text-black">Komentar</Label>
<div className="mt-4 border p-4 rounded bg-gray-50">
<Textarea
placeholder="Tulis tanggapan Anda di sini..."
value={message}
onChange={handleInputChange}
/>
<div className="flex justify-end mt-3">
<Button
color="primary"
onClick={() => postData()}
type="button"
>
Kirim Komentar
</Button>
</div>
</div>
{listData?.map((item: any) => (
<div key={item.id} className="flex flex-col gap-3 mt-2 ">
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${item.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">{item.message}</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(item.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(item.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === item.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${item.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(item.id)}
>
Kirim
</button>
</div>
)}
{item.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col">
{item.children.map((child: any) => (
<div
key={child.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child.message}
</p>
<div className="flex flex-row gap-2">
{/* <div
className="flex items-center mt-1 text-blue-500 cursor-pointer"
onClick={() => handleReply(child.id)}
>
<DotSquare className="w-4 h-4" />
<span className="ml-1">Balas</span>
</div> */}
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() => deleteData(child.id)}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">Delete</span>
</div>
</div>
</div>
</div>
{replyingTo === child.id && (
<div className="ml-10 mt-2">
<textarea
id={`input-comment-${child.id}`}
className="w-full p-2 border rounded"
placeholder="Masukkan tanggapan anda"
/>
<button
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
onClick={() => sendReplyData(child.id)}
>
Kirim
</button>
</div>
)}
{child.children?.length > 0 && (
<div className="ml-10 mt-2 flex flex-col mb-3">
{child.children.map((child2: any) => (
<div
key={child2.id}
className="flex flex-col gap-3 mt-2"
>
<div className="flex flex-row gap-3 ">
<Avatar className="mt-2">
<AvatarImage
src={"/assets/avatar-profile.png"}
alt={`@${child2.username}`}
/>
</Avatar>
<div className="flex flex-col bg-slate-200 w-full px-2 py-2 rounded-md">
<div className="flex items-center justify-between">
<span className="text-gray-700 font-semibold">
{item.messageFrom.fullname}
</span>
<span className="text-gray-500 text-sm">
{formatDate(item.createdAt)}
</span>
</div>
<p className="text-gray-800 mt-1">
{child2.message}
</p>
<div className="flex flex-row gap-2">
<div
className="flex items-center mt-1 text-red-500 cursor-pointer"
onClick={() =>
deleteData(child2.id)
}
>
<TrashIcon className="w-4 h-4" />
<span className="ml-1">
Delete
</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</CardContent>
</div>
</Card>
) : (
""
)}
</div>
);
}

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Kurasi Konten",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,149 @@
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowRight, Rows, Search, UploadIcon } from "lucide-react";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";
import { Link } from "@/components/navigation";
import { formatDateToIndonesian, generateLocalizedPath } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
import { locale } from "dayjs";
import { useEffect, useState } from "react";
import { getListContent } from "@/service/landing/landing";
import GiatRoutine from "./giat-routine/video/audio-visual";
import VideoSliderPage from "./giat-routine/video/audio-visual";
import AudioSliderPage from "./giat-routine/audio/audio";
import ImageSliderPage from "./giat-routine/image/image";
import TeksSliderPage from "./giat-routine/document/teks";
import ContestTable from "../contest/components/contest-table";
import { useTranslations } from "next-intl";
const CuratedContentPage = () => {
const t = useTranslations("Curation");
return (
<div>
<SiteBreadcrumb />
<div className="my-3">
<Tabs defaultValue="giat-routine" className="w-full">
<Card className="py-3 px-2 my-4">
<p className="text-lg font-semibold ml-2">
{t("content-curation", { defaultValue: "Content Curation" })}
</p>
<TabsList className="flex-wrap">
<TabsTrigger
value="giat-routine"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
Giat Rutin
</TabsTrigger>
<TabsTrigger
value="giat-penugasan"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
Giat Penugasan
</TabsTrigger>
<TabsTrigger
value="contest"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground rounded-md px-6"
>
Lomba
</TabsTrigger>
</TabsList>
</Card>
<TabsContent value="giat-routine">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="giat-penugasan">
<div className="grid grid-cols-12 gap-5">
<div className="lg:col-span-12 col-span-12">
<Card>
<div className="flex justify-between items-center py-4 px-5">
<div>
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Judul..."
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
/>
</InputGroup>
</div>
</div>
<div className="ml-5 pb-3">
<div className="px-5 my-5">
<VideoSliderPage />
</div>
<div className="px-5 my-5">
<AudioSliderPage />
</div>
<div className="px-5 my-5">
<ImageSliderPage />
</div>
<div className="px-5 my-5">
<TeksSliderPage />
</div>
</div>
</Card>
</div>
</div>
</TabsContent>
<TabsContent value="contest">
<Card>
<div className="py-3">
<ContestTable />
</div>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default CuratedContentPage;

View File

@ -0,0 +1,321 @@
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { Link } from "@/components/navigation";
import { useRouter } from "next/navigation";
import { deleteTask } from "@/service/task";
import { error, loading } from "@/lib/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useTranslations } from "next-intl";
const useTableColumns = () => {
const t = useTranslations("Table");
const columns: ColumnDef<any>[] = [
{
accessorKey: "no",
header: t("no", { defaultValue: "No" }),
cell: ({ row }) => <span>{row.getValue("no")}</span>,
},
{
accessorKey: "title",
header: t("title", { defaultValue: "Title" }),
cell: ({ row }) => (
<div>
<span>{row.getValue("title")}</span>
{row.original.isForward && (
<Button
variant={"outline"}
color="primary"
size="sm"
className="ml-3 rounded-xl"
>
Forward
</Button>
)}
</div>
),
},
{
accessorKey: "uniqueCode",
header: t("code", { defaultValue: "Code" }),
cell: ({ row }) => <span>{row.getValue("uniqueCode")}</span>,
},
{
accessorKey: "assignmentMainType",
header: t("type-task", { defaultValue: "Type Task" }),
cell: ({ row }) => {
const type = row.getValue("assignmentMainType") as { name: string };
return <span>{type?.name}</span>;
},
},
{
accessorKey: "assignmentType",
header: t("category-task", { defaultValue: "Category Task" }),
cell: ({ row }) => {
const type = row.getValue("assignmentType") as { name: string };
return <span>{type?.name}</span>;
},
},
{
accessorKey: "createdAt",
header: t("upload-date", { defaultValue: "Upload Date" }),
cell: ({ row }) => {
const createdAt = row.getValue("createdAt") as
| string
| number
| undefined;
const formattedDate =
createdAt && !isNaN(new Date(createdAt).getTime())
? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const isActive = row.original.isActive;
const isDone = row.original.isDone;
let statusText = "";
if (isDone) {
statusText = "Selesai";
} else if (isActive) {
statusText = "Aktif";
} else {
statusText = "Nonaktif";
}
const statusColors: Record<string, string> = {
Aktif: "bg-primary/20 text-primary",
Selesai: "bg-success/20 text-success",
Nonaktif: "bg-gray-200 text-gray-500",
};
const statusStyles = statusColors[statusText] || "default";
return (
<Badge className={cn("rounded-full px-5", statusStyles)}>
{statusText}
</Badge>
);
},
},
{
id: "actions",
accessorKey: "action",
header: t("action", { defaultValue: "Action" }),
enableHiding: false,
// cell: ({ row }) => {
// const router = useRouter();
// const MySwal = withReactContent(Swal);
// async function deleteProcess(id: any) {
// loading();
// const resDelete = await deleteTask(id);
// if (resDelete?.error) {
// error(resDelete.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 TaskDelete = (id: any) => {
// MySwal.fire({
// title: "Hapus Data",
// text: "",
// icon: "warning",
// showCancelButton: true,
// cancelButtonColor: "#3085d6",
// confirmButtonColor: "#d33",
// confirmButtonText: "Hapus",
// }).then((result) => {
// if (result.isConfirmed) {
// deleteProcess(id);
// }
// });
// };
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button
// size="icon"
// className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
// >
// <span className="sr-only">Open menu</span>
// <MoreVertical className="h-4 w-4 text-default-800" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent className="p-0" align="end">
// <Link href={`/contributor/task/detail/${row.original.id}`}>
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
// <Eye className="w-4 h-4 me-1.5" />
// View
// </DropdownMenuItem>
// </Link>
// <Link href={`/contributor/task/update/${row.original.id}`}>
// <DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
// <SquarePen className="w-4 h-4 me-1.5" />
// Edit
// </DropdownMenuItem>
// </Link>
// <DropdownMenuItem
// onClick={() => TaskDelete(row.original.id)}
// className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
// >
// <Trash2 className="w-4 h-4 me-1.5" />
// Delete
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
cell: ({ row }) => {
const router = useRouter();
const MySwal = withReactContent(Swal);
const roleId = getCookiesDecrypt("urie");
const levelNumber = getCookiesDecrypt("ulne");
const userLevelId = getCookiesDecrypt("ulie");
const contentLevelNumber =
row.original.createdBy?.userLevel?.levelNumber;
const isSameLevelOrLower =
levelNumber &&
contentLevelNumber &&
+levelNumber <= +contentLevelNumber;
async function deleteProcess(id: any) {
loading();
const resDelete = await deleteTask(id);
if (resDelete?.error) {
error(resDelete.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 TaskDelete = (id: any) => {
MySwal.fire({
title: "Hapus Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
deleteProcess(id);
}
});
};
const isDone = row.original.isDone;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0" align="end">
<Link href={`/contributor/task/detail/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{/* {!isDone && (
<>
<Link href={`/contributor/task/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => TaskDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</>
)} */}
{!isDone && isSameLevelOrLower && (
<>
<Link href={`/contributor/task/update/${row.original.id}`}>
<DropdownMenuItem className="p-2 border-b text-default-700 group focus:bg-default focus:text-primary-foreground rounded-none">
<SquarePen className="w-4 h-4 me-1.5" />
Edit
</DropdownMenuItem>
</Link>
<DropdownMenuItem
onClick={() => TaskDelete(row.original.id)}
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none"
>
<Trash2 className="w-4 h-4 me-1.5" />
Delete
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default useTableColumns;

View File

@ -0,0 +1,420 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
ChevronLeft,
ChevronRight,
Eye,
MoreVertical,
Search,
SquarePen,
Trash2,
TrendingDown,
TrendingUp,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useRouter, useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import { listTask } from "@/service/task";
import { Label } from "@/components/ui/label";
import { format } from "date-fns";
import { useTranslations } from "next-intl";
import useTableColumns from "./columns";
const TaskTable = () => {
const router = useRouter();
const searchParams = useSearchParams();
const t = useTranslations("AnalyticsDashboard");
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [statusFilter, setStatusFilter] = React.useState<number[]>([]);
const [dateFilter, setDateFilter] = React.useState("");
const [endDate, setEndDate] = React.useState("");
const [filterByCode, setFilterByCode] = React.useState<string>("");
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [limit, setLimit] = React.useState(10);
const [isSpecificAttention, setIsSpecificAttention] = React.useState(true);
const [search, setSearch] = React.useState<string>("");
const columns = useTableColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchData();
}, [
page,
showData,
isSpecificAttention,
search,
dateFilter,
filterByCode,
statusFilter,
]);
async function fetchData() {
const formattedStartDate = dateFilter
? format(new Date(dateFilter), "yyyy-MM-dd")
: "";
try {
const res = await listTask(
page - 1,
search,
showData,
filterByCode,
formattedStartDate,
isSpecificAttention ? "atensi-khusus" : "tugas-harian",
statusFilter
);
const data = res?.data?.data;
const contentData = data?.content || [];
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
console.log("contentData : ", contentData);
setDataTable(contentData);
setTotalData(data?.totalElements || 0);
setTotalPage(data?.totalPages || 1);
} catch (error) {
console.error("Error fetching tasks:", error);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterByCode(e.target.value);
setSearch(e.target.value);
table.getColumn("judul")?.setFilterValue(e.target.value);
};
function handleStatusCheckboxChange(value: number) {
setStatusFilter((prev) =>
prev.includes(value)
? prev.filter((status) => status !== value)
: [...prev, value]
);
}
// const handleSearchFilterByCode = (e: React.ChangeEvent<HTMLInputElement>) => {
// const value = e.target.value;
// console.log("code :", value);
// setFilterByCode(value);
// fetchData();
// };
return (
<div className="w-full overflow-x-auto">
<div className="mx-5 mb-3">
<div className="">
<div className="row">
<div className="flex justify-between mb-6">
<label className="inline-flex text-md cursor-pointer">
<input
type="checkbox"
onChange={() => setIsSpecificAttention(!isSpecificAttention)}
hidden
/>
<span
className={` ${
isSpecificAttention
? "bg-default-900 text-white dark:text-black border bg-black"
: "dark:text-default-700 border-2 dark:border dark:border-gray-500"
}
px-[18px] py-1 transition duration-100 rounded`}
>
{t("special-attention", {
defaultValue: "Special Attention",
})}
</span>
<span
className={`
${
!isSpecificAttention
? "bg-default-900 text-white dark:text-black border bg-black"
: " dark:text-default-700 border-2 dark:border dark:border-gray-500"
}
px-[18px] py-1 transition duration-100 rounded
`}
>
{t("daily-tasks", { defaultValue: "Daily Tasks" })}
</span>
</label>
</div>
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row lg:flex-row justify-between sm:items-center md:items-center lg:items-center px-5">
<div className="mb-3 sm:mb-0 lg-mb-0">
<InputGroup merged>
<InputGroupText className="bg-transparent dark:border-secondary dark:group-focus-within:border-secondary">
<Search className=" h-4 w-4 dark:text-white" />
</InputGroupText>
<Input
type="text"
placeholder="Search Title dan Code"
className="bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white w-full"
value={search}
onChange={handleSearch}
/>
</InputGroup>
</div>
<div className=" flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
1 - {showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
1 - 10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
1 - 50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
1 - 100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
1 - 250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="ml-auto w-full sm:w-[100px]"
size="md"
>
Filter <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-64 h-[200px] overflow-y-auto"
>
<div className="flex flex-row justify-between my-1 mx-1">
<p>Filter</p>
</div>
<div className="mx-2 my-1">
<Label>{t("date", { defaultValue: "Date" })}</Label>
<Input
type="date"
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
className="max-w-sm"
/>
</div>
{/* <div className="mx-2 my-1">
<Label>Code</Label>
<Input
placeholder="Filter Status..."
value={filterByCode}
// onChange={handleSearchFilterByCode}
className="max-w-sm"
/>
</div> */}
<Label className="ml-2 mt-2">Status</Label>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-1"
className="mr-2"
checked={statusFilter.includes(1)}
onChange={() => handleStatusCheckboxChange(1)}
/>
<label htmlFor="status-1" className="text-sm">
{t("done", { defaultValue: "Done" })}
</label>
</div>
<div className="flex items-center px-4 py-1">
<input
type="checkbox"
id="status-2"
className="mr-2"
checked={statusFilter.includes(2)}
onChange={() => handleStatusCheckboxChange(2)}
/>
<label htmlFor="status-2" className="text-sm">
{t("active", { defaultValue: "Active" })}
</label>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* <div className="flex-none">
<Input
placeholder="Filter Status..."
value={
(table.getColumn("status")?.getFilterValue() as string) ?? ""
}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
table.getColumn("status")?.setFilterValue(event.target.value)
}
className="max-w-sm "
/>
</div> */}
</div>
<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>
);
};
export default TaskTable;

View File

@ -0,0 +1,16 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
const TaskCreatePage = () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormTask />
</div>
</div>
);
};
export default TaskCreatePage;

View File

@ -0,0 +1,17 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
const TaskDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormTaskDetail />
</div>
</div>
);
};
export default TaskDetailPage;

View File

@ -0,0 +1,18 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
import FormTaskForward from "@/components/form/task/task-forward-form";
const TaskForwardPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormTaskForward />
</div>
</div>
);
};
export default TaskForwardPage;

View File

@ -0,0 +1,9 @@
export const metadata = {
title: "Task",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default Layout;

View File

@ -0,0 +1,54 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import TaskTable from "./components/task-table";
import { Button } from "@/components/ui/button";
import { UploadIcon } from "lucide-react";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import { Link } from "@/components/navigation";
import { checkAuthorization, checkLoginSession } from "@/lib/utils";
import React, { useEffect } from "react";
import { useTranslations } from "next-intl";
const TaskPage = () => {
const t = useTranslations("AnalyticsDashboard");
useEffect(() => {
function initState() {
checkAuthorization("admin");
checkLoginSession();
}
initState();
}, []);
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<Card className="bg-slate-100 border">
<CardHeader className="border-b border-solid border-default-200 mb-6 ">
<CardTitle>
<div className="flex flex-col sm:flex-row lg:flex-row lg:items-center">
<div className="flex-1 text-xl font-medium text-default-900">
{t("tabel", { defaultValue: "Tabel" })} {t("task", { defaultValue: "Task" })}
</div>
<div className="flex-none">
<Link href={"/admin/task/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
{t("create-task", { defaultValue: "Create Task" })}
</Button>
</Link>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TaskTable />
</CardContent>
</Card>
</div>
</div>
);
};
export default TaskPage;

View File

@ -0,0 +1,18 @@
import { Card, CardContent } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import FormTask from "@/components/form/task/task-form";
import FormTaskDetail from "@/components/form/task/task-detail-form";
import FormTaskEdit from "@/components/form/task/task-edit-form";
const TaskDetailPage = async () => {
return (
<div>
<SiteBreadcrumb />
<div className="space-y-4">
<FormTaskEdit />
</div>
</div>
);
};
export default TaskDetailPage;

View File

@ -86,7 +86,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
<div className="text-center mb-8">
<div className=" w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-8">
<img
src="/logo-netidhub.png"
src="/assets/logo1.png"
alt="netidhub Logo"
className="max-w-[150px] h-auto drop-shadow-lg"
/>
@ -199,7 +199,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
type="submit"
fullWidth
disabled={isSubmitting}
className="mt-6 bg-[#C6A455]"
className="mt-6 bg-red-700"
color="primary"
>
{isSubmitting ? (

View File

@ -0,0 +1,170 @@
"use client";
import React, { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { formatDateToIndonesian } from "@/utils/globals";
import { getArticleCategoryDetail } from "@/service/categories/categories";
type CategoryDetail = {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
parentId: number;
createdById: number;
createdByName?: string;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
};
export default function CategoriesDetailForm() {
const { id } = useParams() as { id: string };
const router = useRouter();
const [detail, setDetail] = useState<CategoryDetail | null>(null);
useEffect(() => {
async function init() {
if (id) {
try {
const res = await getArticleCategoryDetail(Number(id));
setDetail(res?.data?.data);
} catch (err) {
console.error("Error fetching category detail:", err);
}
}
}
init();
}, [id]);
return (
<form>
{detail ? (
<div className="flex flex-col lg:flex-row gap-10">
{/* MAIN FORM */}
<Card className="w-full lg:w-8/12 px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Category Detail</p>
{/* Title */}
<div className="space-y-2 py-3">
<Label>Title</Label>
<Input type="text" value={detail.title} readOnly />
</div>
{/* Slug */}
<div className="space-y-2 py-3">
<Label>Slug</Label>
<Input type="text" value={detail.slug || "-"} readOnly />
</div>
{/* Description */}
<div className="space-y-2 py-3">
<Label>Description</Label>
<textarea
value={detail.description}
className="w-full border rounded-md p-2"
readOnly
/>
</div>
{/* Thumbnail */}
<div className="space-y-2 py-3">
<Label>Thumbnail</Label>
<Card className="mt-2 w-fit">
<img
src={detail.thumbnailUrl}
alt="Category Thumbnail"
className="h-[200px] rounded"
/>
</Card>
</div>
{/* Tags */}
<div className="space-y-2 py-3">
<Label>Tags</Label>
<div className="flex flex-wrap gap-2">
{detail?.tags?.length > 0 ? (
detail.tags.map((tag: string, i: number) => (
<Badge key={i} className="px-2 py-1 rounded-md">
{tag}
</Badge>
))
) : (
<span className="text-slate-400 text-sm">No tags</span>
)}
</div>
</div>
</Card>
{/* SIDEBAR */}
<div className="w-full lg:w-4/12">
<Card className="px-4 py-4 space-y-4">
{/* Creator */}
<div>
<Label>Created By</Label>
<Input
type="text"
value={detail.createdByName || detail.createdById}
readOnly
/>
</div>
{/* Status */}
<div>
<Label>Status</Label>
<p className="text-sm text-slate-600">
{detail.isPublish ? "Published" : "Draft"} |{" "}
{detail.isActive ? "Active" : "Inactive"}
</p>
</div>
{/* Published Date */}
{detail.publishedAt && (
<div>
<Label>Published At</Label>
<p className="text-sm">
{formatDateToIndonesian(new Date(detail.createdAt))}
</p>
</div>
)}
{/* Created & Updated */}
<div>
<Label>Created At</Label>
<p className="text-sm">
{formatDateToIndonesian(new Date(detail.createdAt))}
</p>
</div>
<div>
<Label>Updated At</Label>
<p className="text-sm">
{formatDateToIndonesian(new Date(detail.updatedAt))}
</p>
</div>
{/* Back Button */}
<Button
type="button"
onClick={() => router.push("/admin/categories")}
className="mt-4"
>
Back to Categories
</Button>
</Card>
</div>
</div>
) : (
<p className="text-center text-slate-500">Loading...</p>
)}
</form>
);
}

View File

@ -0,0 +1,456 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useRouter } from "next/navigation";
import makeAnimated from "react-select/animated";
import Select from "react-select";
import {
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@radix-ui/react-select";
import { SelectGroup } from "@/components/ui/select";
import dynamic from "next/dynamic";
import { getCuratorUser, getTicketingPriority, saveTicketing } from "@/service/service/communication/communication";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
naration: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
interface Option {
id: string;
label: string;
value: string;
fullname: string;
userLevel: string;
userLevelId: string;
}
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
export default function FormCollaboration() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
type TaskSchema = z.infer<typeof taskSchema>;
const [isSubmitting, setIsSubmitting] = useState(false);
const [taskOutput, setTaskOutput] = useState({
all: false,
video: false,
audio: false,
image: false,
text: false,
});
const [assignmentType, setAssignmentType] = useState("mediahub");
const [assignmentCategory, setAssignmentCategory] = useState("publication");
const [mainType, setMainType] = useState<number>(1);
const [type, setType] = useState<string>("1");
const [options, setOptions] = useState<Option[]>([]);
const [ticketPriority, setTicketPriority] = useState<
{ value: number; label: string }[]
>([]);
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined
);
const animatedComponent = makeAnimated();
const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
const priority = [
{ value: "low", label: "Low" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
];
const {
control,
handleSubmit,
formState: { errors },
} = useForm<TaskSchema>({
resolver: zodResolver(taskSchema),
});
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedValue = Number(event.target.value);
setMainType(selectedValue);
setPlatformTypeVisible(selectedValue === 2);
};
useEffect(() => {
getUser();
getTicketPriority();
});
const handleChange = (e: any) => {
const selected = e;
setSelectedOption(selected);
};
const formatOptionLabel = (option: Option) => (
<>
<div className="row">
<div className="col">
{option.value} | {option.fullname}
</div>
</div>
<div className="row">
<div className="col">
<b>{option.userLevel}</b>
</div>
</div>
</>
);
async function getTicketPriority() {
const res = await getTicketingPriority();
if (res?.data !== null) {
const rawData = res?.data?.data;
const priorityOptions = rawData.map((item: any) => ({
value: item.id,
label: item.name,
}));
setTicketPriority(priorityOptions);
}
}
async function getUser() {
const res = await getCuratorUser();
if (res?.data !== null) {
const rawUser = res?.data?.data?.content;
console.log("raw user", rawUser);
const optionArr: Option[] = rawUser.map((option: any) => ({
id: option?.id,
label: option?.username + option?.fullname + option?.userLevel?.name,
value: option?.username,
fullname: option?.fullname,
userLevel: option?.userLevel?.name,
userLevelId: option?.userLevel?.id,
}));
setOptions(optionArr);
}
}
const save = async (data: TaskSchema) => {
setIsSubmitting(true);
MySwal.fire({
title: "Menyimpan...",
text: "Mohon tunggu sebentar",
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
Swal.showLoading();
},
});
const requestData = {
title: data.title,
typeId: 11,
statusId: 1,
message: data.naration,
priorityId: selectedTarget,
isCollaboration: true,
isEscalation: true,
isCollaborationWithNoneTicket: true,
operatorTeam: selectedOption?.id,
};
const response = await saveTicketing(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
Swal.close();
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/in/supervisor/communications/collaboration");
});
setIsSubmitting(false);
};
const onSubmit = (data: TaskSchema) => {
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);
}
});
};
return (
<Card>
<div className="px-6 py-6 border rounded-lg bg-slate-100">
<p className="text-lg font-semibold mb-3">Form Penugasan</p>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="w-full">
<div className="mt-5">
<Label>
Priority<span className="text-red-500">*</span>
</Label>
<Select
id="target-select"
options={ticketPriority}
onChange={(selectedOption) =>
setSelectedTarget(Number(selectedOption?.value))
}
placeholder="Pilih"
styles={{
control: (base, state) => ({
...base,
minHeight: "40px",
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937" // gray-800
: "#ffffff",
color: document.documentElement.classList.contains("dark")
? "#f9fafb" // gray-100
: "#111827", // gray-900
borderColor: state.isFocused
? "#2563eb"
: base.borderColor, // biru-600
boxShadow: state.isFocused
? "0 0 0 1px #2563eb"
: base.boxShadow,
"&:hover": {
borderColor: "#2563eb",
},
}),
menu: (base) => ({
...base,
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected
? "#2563eb" // biru solid kalau dipilih
: state.isFocused
? "#2563eb33" // biru transparan kalau hover
: document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: state.isSelected
? "#ffffff"
: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
cursor: "pointer",
}),
singleValue: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
placeholder: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#9ca3af"
: "#6b7280",
}),
}}
/>
</div>
{/* <div className="mt-5">
<Label>
Priority<span className="text-red-500">*</span>
</Label>
<Select
className="bg-white dark:bg-black"
id="target-select"
options={ticketPriority}
onChange={(selectedOption) =>
setSelectedTarget(Number(selectedOption?.value))
}
placeholder="Pilih"
styles={{
control: (base) => ({
...base,
minHeight: "40px",
}),
}}
/>
</div> */}
</div>
<div className="w-full">
<div className="mt-5">
<Label>
Eskalasi Untuk <span className="text-red-500">*</span>
</Label>
<Select
options={options}
className="w-100"
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
formatOptionLabel={formatOptionLabel}
isMulti={false}
styles={{
control: (base, state) => ({
...base,
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937" // bg-gray-800
: "#ffffff", // bg-white
color: document.documentElement.classList.contains("dark")
? "#f9fafb" // text-gray-100
: "#111827", // text-gray-900
borderColor: state.isFocused
? "#2563eb"
: base.borderColor,
boxShadow: state.isFocused
? "0 0 0 1px #2563eb"
: base.boxShadow,
"&:hover": {
borderColor: "#2563eb",
},
}),
menu: (base) => ({
...base,
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected
? "#2563eb"
: state.isFocused
? "#2563eb33"
: document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: state.isSelected
? "#ffffff"
: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
cursor: "pointer",
}),
singleValue: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
placeholder: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#9ca3af"
: "#6b7280",
}),
}}
/>
</div>
{/* <div className="mt-5">
<Label>
Eskalasi Untuk <span className="text-red-500">*</span>
</Label>
<Select
options={options}
className="w-100"
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
formatOptionLabel={formatOptionLabel}
isMulti={false}
/>
</div> */}
</div>
<div className="mt-5">
<Label>Narasi Penugasan</Label>
<Controller
control={control}
name="naration"
render={({ field: { onChange, value } }) => (
<CustomEditor onChange={onChange} initialData={value} />
)}
/>
{errors.naration?.message && (
<p className="text-red-400 text-sm">
{errors.naration.message}
</p>
)}
</div>
</div>
{/* Submit Button */}
<div className="mt-4">
<Button type="submit" color="primary">
Submit
</Button>
</div>
</form>
</div>
</Card>
);
}

View File

@ -0,0 +1,413 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { Textarea } from "@/components/ui/textarea";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Link } from "@/i18n/routing";
import { loading } from "@/lib/swal";
import { id } from "date-fns/locale";
import { htmlToString } from "@/utils/globals";
import InfoLainnyaModal from "../ticketing/info-lainnya";
import { PlusIcon } from "lucide-react";
import { deleteEscalationDiscussion, getEscalationDiscussion, getTicketingDetail, getTicketingInternalDiscussion, saveEscalationDiscussion } from "@/service/service/communication/communication";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
naration: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export default function FormDetailEscalation() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const [detail, setDetail] = useState<any>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [activeReplyId, setActiveReplyId] = useState<number | null>(null);
const [selectedPriority, setSelectedPriority] = useState("");
const [openEmergencyModal, setOpenEmergencyModal] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [listDiscussion, setListDiscussion] = useState([]);
const [message, setMessage] = useState("");
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
if (id) {
const response = await getTicketingDetail(id);
setDetail(response?.data?.data);
}
}
initState();
getTicketReply();
}, [id]);
async function getTicketReply() {
const res = await getTicketingInternalDiscussion(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const handleReply = () => {
setReplyVisible((prev) => !prev);
};
useEffect(() => {
const fetchDiscussion = async () => {
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
};
fetchDiscussion();
}, []);
const postData = async () => {
if (!message.trim()) return;
const payload = {
ticketId: id,
message,
parentId: null,
};
try {
await saveEscalationDiscussion(payload);
setMessage("");
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
} catch (error) {
console.error("Gagal mengirim tanggapan:", error);
}
};
const postDataChild = async (parentId: any) => {
const textarea = document.getElementById(
`input-comment-${parentId}`
) as HTMLTextAreaElement | null;
const replyMessage = textarea?.value;
if (!replyMessage?.trim()) return;
const payload = {
ticketId: Number(id),
parentMessageId: parentId,
message: replyMessage,
};
try {
await saveEscalationDiscussion(payload);
if (textarea) textarea.value = "";
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
} catch (error) {
console.error("Gagal mengirim balasan:", error);
}
};
const openEmergencyIssueDetail = () => {
setOpenEmergencyModal(true);
};
async function deleteDataSuggestion(dataId: any) {
const response = await deleteEscalationDiscussion(dataId);
console.log(response);
const responseGet = await getEscalationDiscussion(id);
setListDiscussion(responseGet?.data?.data);
}
return (
<div>
<div className="flex">
<div className="flex flex-col mt-6 w-full mb-3">
{detail !== undefined && (
<div key={detail?.id} className="bg-slate-300 dark:bg-black rounded-md">
<p className="p-5 bg-slate-300 dark:bg-black rounded-md text-lg font-semibold">
Ticket #{detail.id}
</p>
<div className="flex flex-row gap-3 bg-sky-100 dark:bg-sky-900 p-5 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold">
{detail?.commentFromUserName}
</span>
{` `}
mengirimkan pesan untuk{` `}
<Link
href={
detail?.feed
? detail?.feed?.permalink_url == undefined
? detail?.feedUrl
: detail?.feed?.permalink_url
: ""
}
target="_blank"
className="font-bold"
>
{detail?.message}
</Link>
</p>
<p className="text-xs">
{`${new Date(detail?.createdAt).getDate()}-${
new Date(detail?.createdAt).getMonth() + 1
}-${new Date(detail?.createdAt).getFullYear()} ${new Date(
detail?.createdAt
).getHours()}:${new Date(detail?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-5 bg-white dark:bg-black">{detail.message}</p>
<div className="px-4 py-1 bg-white dark:bg-black text-sm">
{detail?.typeId === 6 && detail?.emergencyIssue ? (
<div className="row mx-0 mb-3 emergency-attachments">
<div className=" mr-4">
<Button
color="primary"
size="md"
onClick={openEmergencyIssueDetail}
>
Info Lainnya
</Button>
</div>
</div>
) : null}
{detail?.emergencyIssue && (
<InfoLainnyaModal
open={openEmergencyModal}
onClose={() => setOpenEmergencyModal(false)}
data={detail.emergencyIssue}
/>
)}
</div>
</div>
)}
</div>
</div>
{detail !== undefined && (
<div className="gap-5 mb-5 w-full border mt-3 rounded-md bg-white dark:bg-black">
<div className="space-y-2 px-3 mt-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={detail?.priority?.name}
>
<SelectTrigger size="md" className="w-3/12">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 px-3 mt-3">
<Label>Description</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Textarea
value={detail?.description}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mx-3 my-3">
<h3 className="text-gray-700 dark:text-white font-medium">Tanggapan</h3>
<div className="space-y-4 ml-5 mt-5">
{listDiscussion.map((parent: any) => (
<div key={parent.id} className="border-b pb-4">
<div className="flex justify-between items-start">
<div>
<p className="font-semibold text-gray-800 dark:text-white">
{parent.messageFrom?.fullname}
</p>
<p className="text-gray-600 dark:text-white">{parent.message}</p>
<p className="text-sm text-gray-400">
{parent.createdAt}
</p>
</div>
<button
onClick={() => deleteDataSuggestion(parent.id)}
className="text-red-500 text-sm hover:underline ml-2"
>
Hapus
</button>
</div>
{activeReplyId !== parent.id && (
<button
onClick={() => setActiveReplyId(parent.id)}
className="text-blue-500 text-sm mt-2 hover:underline"
>
Balas
</button>
)}
{activeReplyId === parent.id && (
<div className="mt-2">
<textarea
id={`input-comment-${parent.id}`}
className="w-full h-20 border border-gray-300 rounded-md p-2"
placeholder="Tulis balasan di sini..."
/>
<div className="flex justify-end gap-2 mt-2">
<button
onClick={() => setActiveReplyId(null)}
className="px-4 py-1 text-sm bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-white rounded-md"
>
Batal
</button>
<button
onClick={() => {
postDataChild(parent.id);
setActiveReplyId(null);
}}
className="px-4 py-1 text-sm bg-blue-600 text-white rounded-md"
>
Kirim
</button>
</div>
</div>
)}
{parent.children?.length > 0 && (
<div className="ml-4 mt-3 space-y-3 border-l-2 pl-4 border-gray-200">
{parent.children.map((child: any) => (
<div
key={child.id}
className="flex justify-between items-start"
>
<div>
<p className="font-semibold text-gray-800 dark:text-white">
{child.messageFrom?.fullname}
</p>
<p className="text-gray-600 dark:text-white">{child.message}</p>
<p className="text-sm text-gray-400">
{child.createdAt}
</p>
</div>
<button
onClick={() => deleteDataSuggestion(child.id)}
className="text-red-500 text-sm hover:underline ml-2"
>
Hapus
</button>
</div>
))}
</div>
)}
</div>
))}
</div>
<div className="mt-6">
<label className="block text-gray-700 dark:text-white font-medium mb-2">
Tulis Tanggapan Anda
</label>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full h-24 border border-gray-300 rounded-md p-2"
placeholder="Tulis tanggapan anda di sini..."
/>
<Button size="sm" variant={"outline"} color="primary">
<PlusIcon />
Lampiran
</Button>
<div className="flex justify-end gap-3 mt-2 mb-3">
<button
onClick={() => setMessage("")}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md"
>
Batal
</button>
<button
onClick={postData}
className="px-4 py-2 bg-blue-600 text-white rounded-md"
>
Kirim Pesan
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,623 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Link } from "@/i18n/routing";
import { loading } from "@/lib/swal";
import { id } from "date-fns/locale";
import { DetailTicket } from "../ticketing/info-lainnya-types";
import { Description } from "@radix-ui/react-toast";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ChevronDownIcon } from "lucide-react";
import { getCookiesDecrypt } from "@/lib/utils";
import { getCuratorUser, getEscalationDiscussion, getTicketingDetail, getTicketingInternalDiscussion, saveEscalationDiscussion, saveTicketsQuestion } from "@/service/service/communication/communication";
import { getOperatorUser } from "@/service/service/management-user/management-user";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
type DiscussionItem = {
id: string;
username: string;
message: string;
createdAt: string;
parentId?: string | null;
};
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export default function FormQuestionsDetail() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const roleId = getCookiesDecrypt("urie");
const [detail, setDetail] = useState<any>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [listDiscussion, setListDiscussion] = useState<DiscussionItem[]>([]);
const [message, setMessage] = useState<string>("");
const [replyMessage, setReplyMessage] = useState<Record<string, string>>({});
const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const [operatorOpt, setOperatorOpt] = useState<
{ id: string; label: string; value: string }[]
>([]);
const [curatorOpt, setCuratorOpt] = useState<
{ id: string; label: string; value: string; badge: string }[]
>([]);
const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
const [selectedOperatorEscalation, setSelectedOperatorEscalation] = useState<
string[]
>([]);
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
// useEffect(() => {
// async function initState() {
// const response = await getQuestionTicket(id);
// setDetail(response?.data?.data);
// if (response?.data !== null) {
// setDetailTickets(response?.data?.data);
// }
// if (detailTickets?.emergencyIssue) {
// reset({
// title: detailTickets.emergencyIssue.title || "",
// description: detailTickets.emergencyIssue.description || "",
// });
// // setSelectedPriority(String(detailTickets.emergencyIssue.urgencyId));
// // setSelectedStatus(String(detailTickets.statusId)); // jika ada
// }
// }
// initState();
// getTicketReply();
// }, [id, reset]);
useEffect(() => {
async function initState() {
const response = await getTicketingDetail(id);
const detail = response?.data?.data;
setDetail(detail);
setDetailTickets(detail);
if (detail?.escalationTeams) {
const teamIds = detail.escalationTeams
.split(":")
.filter((id: string) => id);
setSelectedOperatorEscalation(teamIds);
}
getTicketReply();
}
initState();
}, [id]);
useEffect(() => {
async function getOperator() {
const res = await getOperatorUser(detailTickets?.typeId);
if (res?.data !== null) {
const rawUser = res?.data?.data;
const optionArr = rawUser?.map((option: any) => ({
id: option.id,
label: option.fullName,
value: option.id.toString(), // pastikan string
}));
setOperatorOpt(optionArr);
// 👇 Parse `assignedTeams` ke dalam array string
if (detailTickets?.assignedTeams) {
const assigned = detailTickets.assignedTeams
.split(":")
.filter((id: string) => id); // hapus string kosong
setSelectedOperator(assigned);
}
}
}
getOperator();
}, [detailTickets]);
useEffect(() => {
async function getCurator() {
const res = await getCuratorUser();
const rawUser = res?.data?.data?.content ?? []; // ✅ langsung ambil dari content
const optionArr = rawUser?.map((option: any) => ({
id: option.id,
label: `${option.username} | ${option.fullname}`,
value: option.id.toString(),
badge: option.userLevel?.name ?? "", // optional jika ingin tampilkan badge/tingkatan
}));
setCuratorOpt(optionArr);
}
getCurator();
}, []);
async function getTicketReply() {
const res = await getTicketingInternalDiscussion(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const onSubmit = async (data: any) => {
try {
const payload = {
id,
title: data.title,
description: data.description,
priorityId: selectedPriority,
statusId: selectedStatus,
typeId: detailTickets?.typeId,
parentCommentId: detailTickets?.feedId,
};
const response = await saveTicketsQuestion(payload);
MySwal.fire({
title: "Sukses",
text: "Data berhasil diperbarui.",
icon: "success",
});
// Refresh data jika perlu
getTicketReply();
} catch (error) {
console.error("Gagal update:", error);
MySwal.fire({
title: "Error",
text: "Terjadi kesalahan saat memperbarui.",
icon: "error",
});
}
};
// const handleSendReply = () => {
// if (replyMessage.trim() === "") return;
// const newReply = {
// id: replies.length + 1,
// name: "Mabes Polri - Approver", // Sesuaikan dengan data dinamis jika ada
// message: replyMessage,
// timestamp: new Date().toISOString().slice(0, 19).replace("T", " "),
// };
// setReplies([...replies, newReply]);
// setReplyMessage("");
// };
useEffect(() => {
fetchDiscussions();
}, [id]);
const fetchDiscussions = async () => {
try {
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
} catch (error) {
console.error("Gagal mengambil diskusi", error);
}
};
const sendDiscussionParent = async () => {
if (message?.trim().length === 0) return;
const data = {
ticketId: id,
message,
parentId: null,
};
try {
await saveEscalationDiscussion(data);
setMessage("");
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
} catch (error) {
console.error("Gagal kirim tanggapan", error);
}
};
const sendDiscussionChild = async (parentId: string) => {
const inputMsg = replyMessage[parentId];
if (!inputMsg || inputMsg.trim().length === 0) return;
const data = {
ticketId: id,
message: inputMsg,
parentMessageId: parentId,
};
try {
await saveEscalationDiscussion(data);
const response = await getEscalationDiscussion(id);
setListDiscussion(response?.data?.data || []);
setReplyMessage((prev) => ({ ...prev, [parentId]: "" }));
} catch (error) {
console.error("Gagal kirim balasan", error);
}
};
return (
<div>
<div className="flex">
<div className="flex flex-col mt-6 w-full mb-3">
{detail !== undefined && (
<div key={detail?.id} className="bg-slate-300 rounded-md">
<p className="p-5 bg-slate-300 rounded-md text-lg font-semibold">
Ticket #{detail.id}
</p>
<div className="flex flex-row gap-3 bg-sky-100 p-5 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold">
{detail?.commentFromUserName}
</span>
{` `}
mengirimkan pesan untuk{` `}
<Link
href={
detail?.feed
? detail?.feed?.permalink_url == undefined
? detail?.feedUrl
: detail?.feed?.permalink_url
: ""
}
target="_blank"
className="font-bold"
>
{detail?.message}
</Link>
</p>
<p className="text-xs">
{`${new Date(detail?.createdAt).getDate()}-${
new Date(detail?.createdAt).getMonth() + 1
}-${new Date(detail?.createdAt).getFullYear()} ${new Date(
detail?.createdAt
).getHours()}:${new Date(detail?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-5 bg-white">{detail.message}</p>
</div>
)}
</div>
</div>
{detail !== undefined && (
<div className="gap-5 mb-5 w-full border mt-3 rounded-md bg-white">
<div className="space-y-2 px-3 mt-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={detail?.priority?.name}
>
<SelectTrigger size="md" className="w-3/12">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
{(roleId === "9" || roleId === "10") && (
<div className="mt-5 px-3 mb-3">
<Label>Operator</Label>
{/* Tag yang ditampilkan secara kolom */}
{selectedOperator.length > 0 && (
<div className="flex flex-row gap-2 mb-2">
{selectedOperator.map((id) => {
const label = operatorOpt.find(
(op: any) => op.value === id
)?.label;
return (
<div
key={id}
className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
>
<span>{label}</span>
<button
type="button"
onClick={() =>
setSelectedOperator((prev) =>
prev.filter((val) => val !== id)
)
}
className="ml-2 text-gray-500 hover:text-red-600"
>
×
</button>
</div>
);
})}
</div>
)}
{/* Popover Checkbox Dropdown */}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-between">
Pilih Operator
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full max-h-60 overflow-auto">
<div className="flex flex-col gap-1">
{operatorOpt.map((op: any) => (
<label
key={op.id}
className="flex items-center space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
>
<input
disabled
type="checkbox"
checked={selectedOperator.includes(op.value)}
onChange={(e) => {
if (e.target.checked) {
setSelectedOperator((prev) => [
...prev,
op.value,
]);
} else {
setSelectedOperator((prev) =>
prev.filter((val) => val !== op.value)
);
}
}}
/>
<span>{op.label}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
</div>
)}
<div className="space-y-2 px-3 mt-3">
<Label>Description</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Textarea
value={detail?.description}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
</div>
{(roleId === "9" || roleId === "10") && (
<div className="mt-5 px-3 mb-3">
<Label>Eskalasi Untuk</Label>
{selectedOperatorEscalation.length > 0 && (
<div className="flex flex-row gap-2 mb-2">
{selectedOperatorEscalation.map((id) => {
const operator = curatorOpt.find(
(op: any) => op.value === id
);
if (!operator) return null;
return (
<div
key={id}
className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
>
<div>
<div className="font-semibold">{operator.label}</div>
{operator.badge && (
<div className="text-xs text-gray-500">
{operator.badge}
</div>
)}
</div>
<button
type="button"
onClick={() =>
setSelectedOperatorEscalation((prev) =>
prev.filter((val) => val !== id)
)
}
className="ml-2 text-gray-500 hover:text-red-600"
>
×
</button>
</div>
);
})}
</div>
)}
</div>
)}
<div className="mx-3 my-3">
<h3 className="text-gray-700 font-medium">Tanggapan</h3>
{listDiscussion.map((item: any) => (
<div key={item.id} className="mb-4 border-b pb-2">
<p className="font-semibold">{item.messageFrom?.fullname}</p>
<p>{item.message}</p>
<small className="text-gray-500">{item.createdAt}</small>
<div className="text-sm mt-1">
<button
onClick={() =>
setReplyMessage((prev) => ({
...prev,
[item.id]:
prev[item.id] !== undefined
? ""
: `@${item.messageFrom?.fullname} `,
}))
}
className="text-blue-600"
>
Balas
</button>
</div>
{/* Form Balas */}
{replyMessage[item.id] !== undefined && (
<div className="mt-2">
<textarea
id={`input-comment-${item.id}`}
value={replyMessage[item.id]}
onChange={(e) =>
setReplyMessage((prev) => ({
...prev,
[item.id]: e.target.value,
}))
}
className="w-full h-20 border border-gray-300 rounded-md p-2"
placeholder={`@${item.messageFrom?.fullname}`}
/>
<div className="flex gap-3 mt-1">
<button
onClick={() => sendDiscussionChild(item.id)}
className="text-blue-600"
>
Kirim Pesan
</button>
<button
onClick={() =>
setReplyMessage((prev) => {
const newState = { ...prev };
delete newState[item.id];
return newState;
})
}
className="text-red-500"
>
Cancel
</button>
</div>
</div>
)}
{/* List Balasan */}
{item.children?.map((child: any) => (
<div key={child.id} className="ml-4 mt-2 border-l pl-4">
<p className="font-semibold">
{child.messageFrom?.fullname}
</p>
<p>{child.message}</p>
<small className="text-gray-500">{child.createdAt}</small>
</div>
))}
</div>
))}
{/* Form Tanggapan Baru */}
<div className="mt-4">
<label
htmlFor="replyMessage"
className="block text-gray-700 font-medium mb-2"
>
Tulis Tanggapan Anda
</label>
<textarea
id="replyMessage"
className="w-full h-24 border border-gray-300 rounded-md p-2"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Tulis tanggapan anda di sini..."
/>
<div className="flex justify-end gap-3 mt-2 mb-3">
<button
onClick={() => setMessage("")}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-md"
>
Batal
</button>
<button
onClick={sendDiscussionParent}
className="px-4 py-2 bg-blue-600 text-white rounded-md"
>
Kirim Pesan
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,461 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import { Textarea } from "@/components/ui/textarea";
import { Icon } from "@iconify/react/dist/iconify.js";
import { Link } from "@/i18n/routing";
import { loading } from "@/lib/swal";
import { id } from "date-fns/locale";
import { DetailTicket } from "../ticketing/info-lainnya-types";
import { Description } from "@radix-ui/react-toast";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ChevronDownIcon } from "lucide-react";
import makeAnimated from "react-select/animated";
import Select, { ActionMeta, MultiValue } from "react-select";
import { Checkbox } from "@/components/ui/checkbox";
import { getCuratorUser, getQuestionTicket, getTicketingInternalDiscussion, saveTicketsQuestion } from "@/service/service/communication/communication";
import { getOperatorUser } from "@/service/service/management-user/management-user";
interface Option {
id: string;
label: string;
value: string;
fullname: string;
userLevel: string;
userLevelId: string;
}
// const taskSchema = z.object({
// title: z.string().optional(),
// description: z.string().min(2, {
// message: "Narasi Penugasan harus lebih dari 2 karakter.",
// }),
// });
const taskSchema = z.object({
title: z.string().optional(),
description: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
const optionsData = [
{ value: "1", label: "Low" },
{ value: "2", label: "Medium" },
{ value: "3", label: "High" },
];
type OptionType = {
value: string;
label: string;
};
export default function FormQuestionsForward() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const [detail, setDetail] = useState<any>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [listDiscussion, setListDiscussion] = useState();
const [message, setMessage] = useState("");
const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
// const [selectedPriority, setSelectedPriority] = useState("");
const [selectedPriority, setSelectedPriority] = useState<OptionType | null>(
null
);
const [replyMessage, setReplyMessage] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const [operatorOpt, setOperatorOpt] = useState<
{ id: string; label: string; value: string }[]
>([]);
const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
const [options, setOptions] = useState<Option[]>([]);
const animatedComponent = makeAnimated();
const [isCollaboration, setIsCollaboration] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option[]>([]);
const [replies, setReplies] = useState([
{
id: 1,
name: "Mabes Polri - Approver",
message: "test",
timestamp: "2024-12-20 00:56:10",
},
{
id: 2,
name: "Mabes Polri - Approver",
message: "balas",
timestamp: "2025-01-18 17:42:48",
},
]);
const {
control,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
const response = await getQuestionTicket(id);
setDetail(response?.data?.data);
if (response?.data !== null) {
setDetailTickets(response?.data?.data);
}
if (detailTickets?.emergencyIssue) {
reset({
title: detailTickets.emergencyIssue.title || "",
description: detailTickets.emergencyIssue.description || "",
});
// setSelectedPriority(String(detailTickets.emergencyIssue.urgencyId));
// setSelectedStatus(String(detailTickets.statusId)); // jika ada
}
}
initState();
getTicketReply();
getUser();
}, [id, reset]);
useEffect(() => {
async function getOperator() {
const res = await getOperatorUser(detailTickets?.typeId);
if (res?.data !== null) {
const rawUser = res?.data?.data;
const optionArr = rawUser?.map((option: any) => ({
id: option.id,
label: option.fullName,
value: option.id,
}));
setOperatorOpt(optionArr);
}
}
getOperator();
}, [detailTickets]);
async function getTicketReply() {
const res = await getTicketingInternalDiscussion(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const onSubmit = async (data: any) => {
try {
const payload = {
id,
title: data.title,
description: data.description,
priorityId: selectedPriority?.value,
statusId: selectedStatus,
typeId: detailTickets?.typeId,
parentCommentId: detailTickets?.feedId,
operatorTeam: selectedOperator.join(","),
isEscalation: true,
communicationTeam: selectedOption.map((item) => item.id).join(","),
isCollaboration: isCollaboration,
};
const response = await saveTicketsQuestion(payload);
MySwal.fire({
title: "Sukses",
text: "Data berhasil diperbarui.",
icon: "success",
}).then(() => {
window.location.href = "/in/supervisor/communications/questions/all";
});
getTicketReply();
} catch (error) {
console.error("Gagal update:", error);
MySwal.fire({
title: "Error",
text: "Terjadi kesalahan saat memperbarui.",
icon: "error",
});
}
};
const handleChange = (
selected: MultiValue<Option>,
_actionMeta: ActionMeta<Option>
) => {
setSelectedOption([...selected]);
};
const formatOptionLabel = (option: Option) => (
<>
<div className="row">
<div className="col">
{option.value} | {option.fullname}
</div>
</div>
<div className="row">
<div className="col">
<b>{option.userLevel}</b>
</div>
</div>
</>
);
async function getUser() {
const res = await getCuratorUser();
if (res?.data !== null) {
const rawUser = res?.data?.data?.content;
console.log("raw user", rawUser);
const optionArr: Option[] = rawUser.map((option: any) => ({
id: option?.id,
label: option?.username + option?.fullname + option?.userLevel?.name,
value: option?.username,
fullname: option?.fullname,
userLevel: option?.userLevel?.name,
userLevelId: option?.userLevel?.id,
}));
setOptions(optionArr);
}
}
const handleSendReply = () => {
if (replyMessage.trim() === "") return;
const newReply = {
id: replies.length + 1,
name: "Mabes Polri - Approver",
message: replyMessage,
timestamp: new Date().toISOString().slice(0, 19).replace("T", " "),
};
setReplies([...replies, newReply]);
setReplyMessage("");
};
return (
<div>
<div className="flex">
<div className="flex flex-col mt-6 w-full mb-3">
{detail !== undefined && (
<div key={detail?.id} className="bg-slate-300 rounded-md">
<p className="p-5 bg-slate-300 rounded-md text-lg font-semibold">
Ticket #{detail.id}
</p>
<div className="flex flex-row gap-3 bg-sky-100 p-5 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold">
{detail?.commentFromUserName}
</span>
{` `}
mengirimkan pesan untuk{` `}
<Link
href={
detail?.feed
? detail?.feed?.permalink_url == undefined
? detail?.feedUrl
: detail?.feed?.permalink_url
: ""
}
target="_blank"
className="font-bold"
>
{detail?.message}
</Link>
</p>
<p className="text-xs">
{`${new Date(detail?.createdAt).getDate()}-${
new Date(detail?.createdAt).getMonth() + 1
}-${new Date(detail?.createdAt).getFullYear()} ${new Date(
detail?.createdAt
).getHours()}:${new Date(detail?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-5 bg-white">{detail.message}</p>
</div>
)}
</div>
</div>
{detailTickets && (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5 w-[100%] lg:w-auto border bg-white rounded-md">
<p className="mx-3 mt-3">Properties</p>
<div className="space-y-2 px-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
defaultValue={detailTickets?.message}
type="text"
{...field}
placeholder="Masukkan judul"
/>
)}
/>
</div>
<div className="mt-5 px-3 space-y-3">
<Label>Prioritas</Label>
<Select
options={optionsData}
value={selectedPriority}
onChange={(option) => setSelectedPriority(option)}
placeholder="Pilih Prioritas"
/>
</div>
<div className="mt-5 px-3 mb-3 flex flex-col gap-y-3">
<Label>Operator</Label>
{selectedOperator.length > 0 && (
<div className="flex flex-row gap-2 mb-2">
{selectedOperator.map((id) => {
const label = operatorOpt.find(
(op: any) => op.value === id
)?.label;
return (
<div
key={id}
className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
>
<span>{label}</span>
<button
type="button"
onClick={() =>
setSelectedOperator((prev) =>
prev.filter((val) => val !== id)
)
}
className="ml-2 text-gray-500 hover:text-red-600"
>
×
</button>
</div>
);
})}
</div>
)}
{/* Popover Checkbox Dropdown */}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-3/12 justify-between">
Pilih Operator
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full max-h-60 overflow-auto">
<div className="flex flex-col gap-1">
{operatorOpt.map((op: any) => (
<label
key={op.id}
className="flex space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
>
<input
type="checkbox"
checked={selectedOperator.includes(op.value)}
onChange={(e) => {
if (e.target.checked) {
setSelectedOperator((prev) => [
...prev,
op.value,
]);
} else {
setSelectedOperator((prev) =>
prev.filter((val) => val !== op.value)
);
}
}}
/>
<span>{op.label}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2 px-3 py-3">
<div className="">
<Label>
Eskalasi Untuk <span className="text-red-500">*</span>
</Label>
<Select
options={options}
className="w-100"
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
formatOptionLabel={formatOptionLabel}
isMulti={true}
/>
</div>
</div>
<div className="space-y-2 px-3 py-3">
<Label>Deskripsi</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea {...field} placeholder="Masukkan description" />
)}
/>
</div>
<div className="flex flex-row gap-2 px-3 py-3">
<Label className="">Bagikan Untuk Kolaborasi</Label>
<Checkbox
checked={isCollaboration}
onCheckedChange={(e: boolean) => setIsCollaboration(e)}
/>
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Simpan
</Button>
</div>
</div>
</form>
)}
</div>
);
}

View File

@ -0,0 +1,330 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { htmlToString } from "@/utils/globals";
import { getTicketingInternalDetail, getTicketingInternalDiscussion, saveTicketInternalReply } from "@/service/service/communication/communication";
import { Icon } from "@iconify/react/dist/iconify.js";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
naration: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type taskDetail = {
id: number;
title: string;
createdAt: string;
referenceNumber: string | number;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
status: {
id: number;
name: string;
};
priority: {
id: number;
name: string;
};
broadcastType: string;
narration: string;
is_active: string;
};
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export type internalDetail = {
id: number;
message: string;
createdAt: string;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
};
export default function FormDetailInternal() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const [detail, setDetail] = useState<taskDetail>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
null
);
const [replyVisible, setReplyVisible] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
if (id) {
const response = await getTicketingInternalDetail(id);
setTicketInternal(response?.data?.data || null);
setDetail(response?.data?.data);
}
}
initState();
getTicketReply();
}, [id]);
async function getTicketReply() {
const res = await getTicketingInternalDiscussion(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const handleReply = () => {
setReplyVisible((prev) => !prev);
};
const handleSendReply = async () => {
if (replyMessage.trim() === "") {
MySwal.fire({
title: "Error",
text: "Pesan tidak boleh kosong!",
icon: "error",
});
return;
}
const data = {
ticketId: id,
message: replyMessage,
};
try {
await saveTicketInternalReply(data);
await getTicketReply();
MySwal.fire({
title: "Sukses",
text: "Pesan berhasil dikirim.",
icon: "success",
});
setReplyMessage("");
setReplyVisible(false);
} catch (error) {
MySwal.fire({
title: "Error",
text: "Gagal mengirim balasan.",
icon: "error",
});
console.error("Error sending reply:", error);
}
};
return (
<div className="py-5">
<div className="mt-4 flex flex-row items-center gap-3">
<Button onClick={handleReply} color="default" variant={"outline"}>
Balas
</Button>
<Button color="default" variant={"outline"}>
Hapus
</Button>
</div>
<div className="flex flex-col md:flex-row lg:flex-row gap-5 mt-5">
<div className="flex flex-col w-[100%] lg:w-[70%]">
{replyVisible && (
<div className="">
<textarea
id="replyMessage"
className="w-full h-24 border rounded-md p-2"
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
placeholder="Tulis pesan di sini..."
/>
<div className="flex justify-end gap-3 my-2">
<Button
onClick={() => setReplyVisible(false)}
color="default"
variant="outline"
>
Batal
</Button>
<Button onClick={handleSendReply} color="primary">
Kirim
</Button>
</div>
</div>
)}
<div className="border rounded-t-xl">
<p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
Ticket #{detail?.referenceNumber}
</p>
{ticketReply?.map((list) => (
<div key={list.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold text-sm">
{list?.messageFrom?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">
{`${new Date(list?.createdAt).getDate()}-${
new Date(list?.createdAt).getMonth() + 1
}-${new Date(list?.createdAt).getFullYear()} ${new Date(
list?.createdAt
).getHours()}:${new Date(list?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-4 bg-white text-sm">{list.message}</p>
</div>
))}
{ticketInternal && (
<div key={ticketInternal.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold text-sm">
{ticketInternal?.createdBy?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{ticketInternal?.sendTo?.fullname}
</span>
</p>
<p className="text-xs">
{`${new Date(ticketInternal?.createdAt).getDate()}-${
new Date(ticketInternal?.createdAt).getMonth() + 1
}-${new Date(
ticketInternal?.createdAt
).getFullYear()} ${new Date(
ticketInternal?.createdAt
).getHours()}:${new Date(
ticketInternal?.createdAt
).getMinutes()}`}
</p>
</div>
</div>
<p className="p-4 bg-white text-sm">
{htmlToString(ticketInternal.message)}
</p>
</div>
)}
</div>
</div>
{detail !== undefined && (
<div className="gap-5 mb-5 w-[100%] lg:w-[30%] border bg-white rounded-md">
<p className="mx-3 mt-3">Properties</p>
<div className="space-y-2 px-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={detail?.priority?.name}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Status</Label>
<Select
onValueChange={setSelectedStatus}
value={detail?.status?.name}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="Close">Close</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,339 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import {
getTicketingInternalDetail,
getTicketingInternalDiscussion,
saveTicketing,
saveTicketInternalReply,
} from "@/service/communication/communication";
import { Textarea } from "@/components/ui/textarea";
import { htmlToString } from "@/utils/globals";
import { Icon } from "@iconify/react/dist/iconify.js";
const taskSchema = z.object({
// description: z.string().min(2, {
// message: "Narasi Penugasan harus lebih dari 2 karakter.",
// }),
});
export type replyDetail = {
id: number;
message: string;
createdAt: string;
messageFrom: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export default function FormEditInternal() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const router = useRouter();
const [detail, setDetail] = useState<any>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const [selectedTarget, setSelectedTarget] = useState("");
const [description, setDescription] = useState("");
type TaskSchema = z.infer<typeof taskSchema>;
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
if (id) {
const response = await getTicketingInternalDetail(id);
setDetail(response?.data?.data);
setSelectedPriority(response?.data?.data?.priority?.name);
console.log("sadad", response?.data?.data);
setSelectedStatus(response?.data?.data?.status?.name);
setDescription(htmlToString(response?.data?.data?.message));
}
}
initState();
getTicketReply();
}, [id]);
async function getTicketReply() {
const res = await getTicketingInternalDiscussion(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const handleReply = () => {
setReplyVisible((prev) => !prev); // Toggle visibility
};
const handleSendReply = async () => {
if (replyMessage.trim() === "") {
MySwal.fire({
title: "Error",
text: "Pesan tidak boleh kosong!",
icon: "error",
});
return;
}
const data = {
ticketId: id,
message: replyMessage,
};
try {
const response = await saveTicketInternalReply(data);
// Tambahkan balasan baru ke daftar balasan
const newReply: replyDetail = {
id: response?.data?.id,
message: replyMessage,
createdAt: response?.data?.createdAt,
messageFrom: response?.data?.messageFrom,
messageTo: response?.data?.messageTo,
};
setTicketReply((prevReplies) => [newReply, ...prevReplies]);
MySwal.fire({
title: "Sukses",
text: "Pesan berhasil dikirim.",
icon: "success",
});
// Reset input dan sembunyikan form balasan
setReplyMessage("");
setReplyVisible(false);
} catch (error) {
MySwal.fire({
title: "Error",
text: "Gagal mengirim balasan.",
icon: "error",
});
console.error("Error sending reply:", error);
}
};
const save = async (data: TaskSchema) => {
const requestData = {
// description: data?.description,
target: selectedTarget,
priorityId: 1,
statusId: 1,
// description: data.description,
// operatorTeam: selectedOptionId.join(","), // This should work now without the error
};
const response = await saveTicketing(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/en/shared/communication");
});
};
const onSubmit = (data: TaskSchema) => {
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);
}
});
};
return (
<div className="py-5">
<div className="mt-4 flex flex-row items-center gap-3">
<Button onClick={handleReply} color="default" variant={"outline"}>
Balas
</Button>
<Button color="default" variant={"outline"}>
Hapus
</Button>
</div>
<div className="flex flex-row gap-5 mt-5">
<div className="flex flex-col w-[70%]">
{replyVisible && (
<div>
<textarea
id="replyMessage"
className="w-full h-24 border rounded-md p-2"
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
placeholder="Tulis pesan di sini..."
/>
<div className="flex justify-end gap-3 mt-2">
<Button
onClick={() => setReplyVisible(false)}
color="default"
variant="outline"
>
Batal
</Button>
<Button onClick={handleSendReply} color="primary">
Kirim
</Button>
</div>
</div>
)}
<div className="border rounded-t-xl">
<p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
Ticket #{detail?.referenceNumber}
</p>
{ticketReply?.map((list) => (
<div key={list.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold">
{list?.messageFrom?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">{`${new Date(
list?.createdAt
).getDate()}-${
new Date(list?.createdAt).getMonth() + 1
}-${new Date(list?.createdAt).getFullYear()} ${new Date(
list?.createdAt
).getHours()}:${new Date(
list?.createdAt
).getMinutes()}`}</p>
</div>
</div>
<p className="pl-3 bg-white py-2">{list.message}</p>
</div>
))}
</div>
</div>
{detail !== undefined && (
<form onSubmit={handleSubmit(onSubmit)} className="w-[30%]">
<div className="gap-5 mb-5 border bg-white rounded-xl">
<p className="mx-3 mt-3">Properties</p>
<div className="space-y-2 px-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={selectedPriority}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Status</Label>
<Select
onValueChange={setSelectedStatus}
value={selectedStatus}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="Close">Close</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2 px-3">
<Label>Description</Label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description"
/>
</div>
<div className="flex justify-end mt-3 mr-3">
<Button type="submit" color="primary">
Update
</Button>
</div>
</div>
</form>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,380 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useRouter } from "next/navigation";
import makeAnimated from "react-select/animated";
import Select from "react-select";
import dynamic from "next/dynamic";
import { getCuratorUser, getTicketingPriority, saveTicketingInternal } from "@/service/service/communication/communication";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
message: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
interface Option {
id: string;
label: string;
value: string;
fullname: string;
userLevel: string;
userLevelId: string;
}
const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"),
{ ssr: false }
);
export default function FormInternal() {
const MySwal = withReactContent(Swal);
const router = useRouter();
type TaskSchema = z.infer<typeof taskSchema>;
const [options, setOptions] = useState<Option[]>([]);
const [selectedOption, setSelectedOption] = useState<Option | undefined>();
const [selectedTarget, setSelectedTarget] = useState<number | null>(null);
const animatedComponent = makeAnimated();
const [isSubmitting, setIsSubmitting] = useState(false);
const priority = [
{ value: 1, label: "Low" },
{ value: 2, label: "Medium" },
{ value: 3, label: "High" },
];
const {
control,
handleSubmit,
formState: { errors },
} = useForm<TaskSchema>({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
getUser();
getTicketPriority();
}, []);
const handleChange = (e: any) => {
setSelectedOption(e);
};
async function getTicketPriority() {
const res = await getTicketingPriority();
if (res?.data !== null) {
const rawData = res?.data?.data;
// optional: setTicketPriority(rawData);
}
}
async function getUser() {
const res = await getCuratorUser();
if (res?.data !== null) {
const rawUser = res?.data?.data?.content;
const optionArr: Option[] = rawUser.map((option: any) => ({
id: option?.id,
label: option?.username + option?.fullname + option?.userLevel?.name,
value: option?.username,
fullname: option?.fullname,
userLevel: option?.userLevel?.name,
userLevelId: option?.userLevel?.id,
}));
setOptions(optionArr);
}
}
const save = async (data: TaskSchema) => {
setIsSubmitting(true);
MySwal.fire({
title: "Menyimpan...",
text: "Mohon tunggu, data sedang diproses.",
allowOutsideClick: false,
allowEscapeKey: false,
didOpen: () => {
Swal.showLoading();
},
});
try {
const requestData = {
title: data.title,
message: data.message,
target: selectedTarget,
sendToId: selectedOption?.id,
};
const response = await saveTicketingInternal(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
Swal.close();
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/in/shared/communication");
});
} catch (error) {
Swal.close();
MySwal.fire({
title: "Error",
text: "Terjadi kesalahan saat menyimpan data.",
icon: "error",
});
console.error("Error saat menyimpan data:", error);
} finally {
setIsSubmitting(false);
}
};
const onSubmit = (data: TaskSchema) => {
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);
}
});
};
return (
<Card>
<div className="px-6 py-6 ">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex flex-col justify-center items-center gap-5 mb-5 ">
<div className="w-6/12 space-y-2">
<Label>
Judul<span className="text-red-500">*</span>
</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="w-6/12">
<Label>
Priority <span className="text-red-500">*</span>
</Label>
<Select
id="target-select"
options={priority}
onChange={(selectedOption) =>
setSelectedTarget(selectedOption?.value ?? null)
}
placeholder="Pilih"
styles={{
control: (base, state) => ({
...base,
minHeight: "40px",
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937" // bg-gray-800
: "#ffffff", // bg-white
color: document.documentElement.classList.contains("dark")
? "#f9fafb" // text-gray-100
: "#111827", // text-gray-900
borderColor: state.isFocused ? "#2563eb" : base.borderColor,
boxShadow: state.isFocused
? "0 0 0 1px #2563eb"
: base.boxShadow,
"&:hover": { borderColor: "#2563eb" },
}),
menu: (base) => ({
...base,
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected
? "#2563eb"
: state.isFocused
? "#2563eb33"
: document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: state.isSelected
? "#ffffff"
: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
cursor: "pointer",
}),
singleValue: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
placeholder: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#9ca3af"
: "#6b7280",
}),
}}
/>
</div>
{/* <div className="w-6/12">
<Label>
Priority <span className="text-red-500">*</span>
</Label>
<Select
id="target-select"
options={priority}
onChange={(selectedOption) =>
setSelectedTarget(selectedOption?.value ?? null)
}
placeholder="Pilih"
styles={{ control: (base) => ({ ...base, minHeight: "40px" }) }}
/>
</div> */}
<div className="w-6/12">
<Label>Ditunjukan Untuk</Label>
<Select
options={options}
className="w-100"
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
isMulti={false}
styles={{
control: (base, state) => ({
...base,
minHeight: "40px",
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
borderColor: state.isFocused ? "#2563eb" : base.borderColor,
boxShadow: state.isFocused
? "0 0 0 1px #2563eb"
: base.boxShadow,
"&:hover": { borderColor: "#2563eb" },
}),
menu: (base) => ({
...base,
backgroundColor:
document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected
? "#2563eb"
: state.isFocused
? "#2563eb33"
: document.documentElement.classList.contains("dark")
? "#1f2937"
: "#ffffff",
color: state.isSelected
? "#ffffff"
: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
cursor: "pointer",
}),
singleValue: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#f9fafb"
: "#111827",
}),
placeholder: (base) => ({
...base,
color: document.documentElement.classList.contains("dark")
? "#9ca3af"
: "#6b7280",
}),
}}
/>
</div>
{/* <div className="w-6/12">
<Label>Ditunjukan Untuk</Label>
<Select
options={options}
className="w-100"
closeMenuOnSelect={false}
components={animatedComponent}
onChange={handleChange}
isMulti={false}
/>
</div> */}
<div className="w-6/12 mt-5">
<Label>Narasi Penugasan</Label>
<Controller
control={control}
name="message"
render={({ field: { onChange, value } }) => (
<CustomEditor onChange={onChange} initialData={value} />
)}
/>
{errors.message?.message && (
<p className="text-red-400 text-sm">{errors.message.message}</p>
)}
</div>
<div className="w-6/12 flex justify-end gap-3 mt-5">
<Button type="button" color="primary" variant="outline">
Batal
</Button>
<Button type="submit" color="primary" disabled={isSubmitting}>
{isSubmitting ? "Menyimpan..." : "Submit"}
</Button>
</div>
</div>
</form>
</div>
</Card>
);
}

View File

@ -0,0 +1,670 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Icon } from "@iconify/react/dist/iconify.js";
import { list, parse } from "postcss";
import { htmlToString } from "@/utils/globals";
import { Textarea } from "@/components/ui/textarea";
import { error } from "@/lib/swal";
import { useMediaQuery } from "react-responsive";
import { DetailTicket } from "../ticketing/info-lainnya-types";
import InfoLainnyaModal from "../ticketing/info-lainnya";
import { Description } from "@radix-ui/react-toast";
import { Link, useRouter } from "@/i18n/routing";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ChevronDownIcon } from "lucide-react";
import { deleteTicket, getQuestionTicket, getTicketingReply, saveTicketReply, saveTicketsQuestion } from "@/service/service/communication/communication";
import { getOperatorUser } from "@/service/service/management-user/management-user";
const taskSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
});
export type taskDetail = {
id: number;
title: string;
createdAt: string;
referenceNumber: string | number;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
status: {
id: number;
name: string;
};
priority: {
id: number;
name: string;
};
broadcastType: string;
description: string;
is_active: string;
};
export type replyDetail = {
id: number;
comments: string;
createdAt: string;
user: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export type internalDetail = {
id: number;
message: string;
createdAt: string;
commentFromUserName: string;
feedTitle: string;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
};
export default function FormQuestionsReply() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const router = useRouter();
const isMobile = useMediaQuery({
maxWidth: 768,
});
const [detail, setDetail] = useState<taskDetail>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
null
);
const [detailTicketsQuestions, setDetailTicketsQuestions] =
useState<internalDetail | null>(null);
const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
const [replyVisible, setReplyVisible] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const [openEmergencyModal, setOpenEmergencyModal] = useState(false);
const [replyValue, setReplyValue] = useState<number>(0);
const [replyText, setReplyText] = useState<string>("");
const [operatorOpt, setOperatorOpt] = useState<
{ id: string; label: string; value: string }[]
>([]);
const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
const {
control,
handleSubmit,
reset,
setValue,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
});
useEffect(() => {
async function initState() {
setReplyValue(0);
const response = await getQuestionTicket(id);
setDetailTicketsQuestions(response?.data?.data || null);
setTicketInternal(response?.data?.data || null);
setDetail(response?.data?.data);
setValue('title', response?.data?.data?.message)
if (response?.data !== null) {
setDetailTickets(response?.data?.data);
}
if (detailTickets?.emergencyIssue) {
reset({
title: detailTickets?.emergencyIssue.title || "",
description: detailTickets?.emergencyIssue.description || "",
});
// setSelectedPriority(String(detailTickets.emergencyIssue.urgencyId));
// setSelectedStatus(String(detailTickets.statusId)); // jika ada
}
}
initState();
getTicketReply();
}, [id, reset]);
const handleReply = () => {
setReplyValue((prev) => (prev === 1 ? 0 : 1));
};
const handleSendReplyData = async () => {
if (!replyText.trim()) {
console.warn("Balasan kosong!");
return;
}
try {
const res = await saveTicketReply({
ticketId: id,
comment: replyText,
parentCommentId: detailTickets?.commentId,
isFromInternal: true,
});
console.log("Berhasil kirim balasan:", res?.data);
setReplyText("");
setReplyValue(0);
getTicketReply();
} catch (err) {
console.error("Gagal kirim balasan:", err);
}
};
async function getTicketReply() {
const res = await getTicketingReply(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
useEffect(() => {
async function getOperator() {
const res = await getOperatorUser(detailTickets?.typeId);
if (res?.data !== null) {
const rawUser = res?.data?.data;
const optionArr = rawUser?.map((option: any) => ({
id: option.id,
label: option.fullName,
value: option.id,
}));
setOperatorOpt(optionArr);
}
}
getOperator();
}, [detailTickets]);
const handleSendReply = async () => {
if (replyMessage.trim() === "") {
MySwal.fire({
title: "Error",
text: "Pesan tidak boleh kosong!",
icon: "error",
});
return;
}
const data = {
ticketId: id,
comment: replyMessage,
};
try {
const response = await saveTicketReply(data);
const newReply: replyDetail = {
id: response?.data?.id,
comments: replyMessage,
createdAt: response?.data?.createdAt,
user: response?.data?.messageFrom,
messageTo: response?.data?.messageTo,
};
setTicketReply((prevReplies) => [newReply, ...prevReplies]);
MySwal.fire({
title: "Sukses",
text: "Pesan berhasil dikirim.",
icon: "success",
});
setReplyMessage("");
setReplyVisible(false);
} catch (error) {
MySwal.fire({
title: "Error",
text: "Gagal mengirim balasan.",
icon: "error",
});
console.error("Error sending reply:", error);
}
};
// const onSubmit = async (data: any) => {
// try {
// const payload = {
// id,
// title: data.title,
// description: data.description,
// priorityId: selectedPriority,
// statusId: 1,
// typeId: detailTickets?.typeId,
// parentCommentId: detailTickets?.feedId,
// operatorTeam: selectedOperator.join(","),
// };
// const response = await saveTicketsQuestion(payload);
// MySwal.fire({
// title: "Sukses",
// text: "Data berhasil diperbarui.",
// icon: "success",
// });
// getTicketReply();
// } catch (error) {
// console.error("Gagal update:", error);
// MySwal.fire({
// title: "Error",
// text: "Terjadi kesalahan saat memperbarui.",
// icon: "error",
// });
// }
// };
const onSubmit = async (data: any) => {
try {
MySwal.fire({
title: "Menyimpan...",
text: "Mohon tunggu sebentar",
allowOutsideClick: false,
didOpen: () => {
MySwal.showLoading();
},
});
const payload = {
id,
title: data.title,
description: data.description,
priorityId: selectedPriority,
statusId: 1,
typeId: detailTickets?.typeId,
parentCommentId: detailTickets?.feedId,
operatorTeam: selectedOperator.join(","),
};
await saveTicketsQuestion(payload);
MySwal.fire({
title: "Sukses",
text: "Data berhasil diperbarui.",
icon: "success",
confirmButtonText: "OK",
}).then(() => {
router.push("/supervisor/ticketing");
});
} catch (error) {
MySwal.fire({
title: "Error",
text: "Terjadi kesalahan saat memperbarui.",
icon: "error",
});
}
};
async function doDelete(id: any) {
const response = await deleteTicket(id);
if (response?.error) {
error(response.message);
return false;
}
success("/in/supervisor/ticketing");
}
function success(redirect: string) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
}
const handleDelete = (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);
}
});
};
const openEmergencyIssueDetail = () => {
setOpenEmergencyModal(true);
};
return (
<div className="py-5">
<div className="mt-4 flex flex-row items-center gap-3">
<div className="mt-4 flex flex-row items-center gap-3">
<Link href={`/supervisor/communications/escalation/forward/${id}`}>
<Button color="default" variant={"outline"}>
Eskalasi
</Button>
</Link>
</div>
</div>
<div className="flex flex-col md:flex-row lg:flex-row gap-5 mt-5">
<div className="flex flex-col w-[100%] lg:w-[70%]">
{replyVisible && (
<div className="">
<textarea
id="replyMessage"
className="w-full h-24 border rounded-md p-2"
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
placeholder="Tulis pesan di sini..."
/>
<div className="flex justify-end gap-3 my-2">
<Button
onClick={() => setReplyVisible(false)}
color="default"
variant="outline"
>
Batal
</Button>
<Button onClick={handleSendReply} color="primary">
Kirim
</Button>
</div>
</div>
)}
<div className="border rounded-t-xl">
<p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
Ticket #{detail?.referenceNumber}
</p>
{ticketReply?.map((list) => (
<div key={list.id} className="flex flex-col mb-4">
{isMobile ? (
<div className="flex gap-3 bg-sky-100 p-3 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p className="text-sm">
<span className="font-bold">
{list?.user?.fullname}
</span>{" "}
mengirimkan balasan{" "}
<span className="font-bold">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">
{new Date(list.createdAt).toLocaleString("id-ID", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div>
) : (
<div className="flex gap-4 bg-sky-100 p-4 items-center">
<div className="text-center">
<Icon icon="qlementine-icons:user-16" width={50} />
</div>
<div>
<p className="text-sm">
<span className="font-bold">
{list?.user?.fullname}
</span>{" "}
mengirimkan balasan{" "}
<span className="font-bold">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">
{new Date(list.createdAt).toLocaleString("id-ID", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div>
)}
<div className="bg-white text-sm p-4">{list.comments}</div>
</div>
))}
{detailTicketsQuestions && (
<div key={detailTicketsQuestions.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold text-sm">
{detailTicketsQuestions?.commentFromUserName}
</span>{" "}
mengirimkan komentar untuk{" "}
<span className="font-bold text-sm">
{detailTicketsQuestions?.feedTitle}
</span>
</p>
<p className="text-xs">
{`${new Date(
detailTicketsQuestions?.createdAt
).getDate()}-${
new Date(detailTicketsQuestions?.createdAt).getMonth() +
1
}-${new Date(
detailTicketsQuestions?.createdAt
).getFullYear()} ${new Date(
detailTicketsQuestions?.createdAt
).getHours()}:${new Date(
detailTicketsQuestions?.createdAt
).getMinutes()}`}
</p>
</div>
</div>
<div className="p-4 bg-white text-sm">
<p>{htmlToString(detailTicketsQuestions.message)}</p>
{detailTickets?.typeId === 6 &&
detailTickets?.emergencyIssue ? (
<div className="row mx-0 mb-3 emergency-attachments">
<div className="mt-3 mr-4">
<Button
color="primary"
size="md"
onClick={openEmergencyIssueDetail}
>
Info Lainnya
</Button>
</div>
</div>
) : null}
{detailTickets?.emergencyIssue && (
<InfoLainnyaModal
open={openEmergencyModal}
onClose={() => setOpenEmergencyModal(false)}
data={detailTickets.emergencyIssue}
/>
)}
</div>
</div>
)}
</div>
</div>
{detailTickets && (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5 w-[100%] lg:w-auto border bg-white rounded-md">
<p className="mx-3 mt-3">Properties</p>
<div className="space-y-2 px-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
// defaultValue={detailTickets?.message}
value={field.value}
// {...field}
onChange={field.onChange}
placeholder="Masukkan judul"
/>
)}
/>
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={selectedPriority}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih Prioritas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Low</SelectItem>
<SelectItem value="2">Medium</SelectItem>
<SelectItem value="3">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Operator</Label>
{/* Tag yang ditampilkan secara kolom */}
{selectedOperator.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{selectedOperator.map((id) => {
const label = operatorOpt.find(
(op: any) => op.value === id
)?.label;
return (
<div
key={id}
className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
>
<span>{label}</span>
<button
type="button"
onClick={() =>
setSelectedOperator((prev) =>
prev.filter((val) => val !== id)
)
}
className="ml-2 text-gray-500 hover:text-red-600"
>
×
</button>
</div>
);
})}
</div>
)}
{/* Popover Checkbox Dropdown */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-between"
>
Pilih Operator
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full max-h-60 overflow-auto">
<div className="flex flex-col gap-1">
{operatorOpt.map((op: any) => (
<label
key={op.id}
className="flex items-center space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
>
<input
type="checkbox"
checked={selectedOperator.includes(op.value)}
onChange={(e) => {
if (e.target.checked) {
setSelectedOperator((prev) => [
...prev,
op.value,
]);
} else {
setSelectedOperator((prev) =>
prev.filter((val) => val !== op.value)
);
}
}}
/>
<span>{op.label}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2 px-3 py-3">
<Label>Deskripsi</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea {...field} placeholder="Masukkan description" />
)}
/>
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Buat Tiket
</Button>
</div>
</div>
</form>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,947 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
createTask,
getTask,
getUserLevelForAssignments,
} from "@/service/task";
import page from "@/app/[locale]/page";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import { CalendarIcon, ChevronDown, ChevronUp, Trash2 } from "lucide-react";
import { format, parseISO } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
import { DateRange } from "react-day-picker";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import dynamic from "next/dynamic";
import Cookies from "js-cookie";
import FileUploader from "../shared/file-uploader";
import { AudioRecorder } from "react-audio-voice-recorder";
import { error, loading } from "@/lib/swal";
import { Upload } from "tus-js-client";
import { getCsrfToken } from "@/service/auth";
import { getOnlyDate } from "@/utils/globals";
import { duration } from "moment";
import { getContestById, postCreateContest } from "@/service/service/contest/contest";
const contestSchema = z.object({
theme: z.string().min(1, { message: "Judul diperlukan" }),
duration: z
.array(z.string().min(1)) // Gunakan array string untuk menyimpan range tanggal
.min(1, { message: "Tanggal diperlukan" }),
hastagCode: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
scoringFormula: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type contestDetail = {
id: number;
theme: string;
hastagCode: string;
assignedToTopLevel: string;
assignmentType: {
id: number;
name: string;
};
assignmentMainType: {
id: number;
name: string;
};
duration: string;
platformType: string | null;
assignmentTypeId: string;
targetOutput: string;
targetParticipantTopLevel: string;
description: string;
fileTypeOutput: any;
is_active: string;
};
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
interface FileWithPreview extends File {
preview: string;
}
export default function FormContestUpdate() {
const MySwal = withReactContent(Swal);
const userRoleId = Number(getCookiesDecrypt("urie"));
const userLevelId = Number(getCookiesDecrypt("ulie"));
const userLevelNumber = Number(getCookiesDecrypt("ulne"));
const router = useRouter();
const editor = useRef(null);
type ContestSchema = z.infer<typeof contestSchema>;
const { id } = useParams() as { id: string };
console.log(id);
const [mainType, setMainType] = useState<string>("1");
const [broadcastType, setBroadcastType] = useState<string>(""); // untuk Tipe Penugasan
const [selectedTarget, setSelectedTarget] = useState("all");
const [detail, setDetail] = useState<any>();
const [refresh] = useState(false);
const [date, setDate] = useState<DateRange | undefined>();
const [listDest, setListDest] = useState([]);
const [checkedLevels, setCheckedLevels] = useState(new Set());
const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false);
const [audioFile, setAudioFile] = useState<File | null>(null);
const [imageFiles, setImageFiles] = useState<FileWithPreview[]>([]);
const [videoFiles, setVideoFiles] = useState<FileWithPreview[]>([]);
const [textFiles, setTextFiles] = useState<FileWithPreview[]>([]);
const [audioFiles, setAudioFiles] = useState<FileWithPreview[]>([]);
const [isImageUploadFinish, setIsImageUploadFinish] = useState(false);
const [isVideoUploadFinish, setIsVideoUploadFinish] = useState(false);
const [isTextUploadFinish, setIsTextUploadFinish] = useState(false);
const [isAudioUploadFinish, setIsAudioUploadFinish] = useState(false);
const [isRecording, setIsRecording] = useState(false);
const [timer, setTimer] = useState<number>(120);
const [links, setLinks] = useState<string[]>([""]);
const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [taskOutput, setTaskOutput] = useState({
all: false,
video: false,
audio: false,
image: false,
text: false,
});
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
satker: false,
});
const {
control,
handleSubmit,
formState: { errors },
} = useForm<ContestSchema>({
resolver: zodResolver(contestSchema),
});
// const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const selectedValue = Number(event.target.value);
// setMainType(selectedValue);
// setPlatformTypeVisible(selectedValue === 2);
// };
useEffect(() => {
async function fetchPoldaPolres() {
setIsLoading(true);
try {
const response = await getUserLevelForAssignments();
setListDest(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();
}, []);
useEffect(() => {
async function initState() {
if (id) {
const response = await getContestById(id);
const details = response?.data?.data;
setDetail(details);
if (details?.duration) {
const [start, end] = details.duration.split(" - "); // Pisahkan tanggal
setDate({
from: parseISO(start),
to: end ? parseISO(end) : undefined, // Pastikan `to` bisa undefined jika hanya satu tanggal
});
}
}
}
initState();
}, [id, refresh]);
useEffect(() => {
if (detail?.targetOutput) {
const outputSet = new Set(detail.targetOutput.split(",").map(Number)); // Membagi string ke dalam array dan mengonversi ke nomor
setTaskOutput({
all: outputSet.has(0),
video: outputSet.has(2),
audio: outputSet.has(4),
image: outputSet.has(1),
text: outputSet.has(3),
});
}
}, [detail?.targetOutput]);
useEffect(() => {
if (detail?.targetOutput) {
const outputSet = new Set(detail.targetOutput.split(",").map(Number));
setUnitSelection({
allUnit: outputSet.has(0),
mabes: outputSet.has(1),
polda: outputSet.has(2),
polres: outputSet.has(3),
satker: outputSet.has(4),
});
}
}, [detail?.targetOutput]);
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(","); // Mengonversi Set ke string
};
const save = async (data: ContestSchema) => {
const fileTypeMapping = {
all: "0",
video: "2",
audio: "4",
image: "1",
text: "3",
};
const unitMapping = {
allUnit: "0",
mabes: "1",
polda: "2",
polres: "3",
satker: "4",
};
const assignmentPurposeString = Object.keys(unitSelection)
.filter((key) => unitSelection[key as keyof typeof unitSelection])
.map((key) => unitMapping[key as keyof typeof unitMapping])
.join(",");
const selectedOutputs = Object.keys(taskOutput)
.filter((key) => taskOutput[key as keyof typeof taskOutput]) // Ambil hanya yang `true`
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping]) // Konversi ke nilai string
.join(",");
// Pastikan `data.duration` ada dan dikonversi ke `Date`
const startDate = data.duration?.[0] ? new Date(data.duration[0]) : null;
const endDate = data.duration?.[1] ? new Date(data.duration[1]) : null;
const formattedDuration = startDate
? endDate
? `${getOnlyDate(startDate)} - ${getOnlyDate(endDate)}`
: getOnlyDate(startDate)
: "";
const requestData: {
id?: any;
theme: string;
duration: string;
targetParticipantTopLevel: any;
targetParticipant: any;
hastagCode: string;
description: string;
scoringFormula: string;
targetOutput: any;
attachmentUrl: string[];
} = {
...data,
id: detail?.id,
hastagCode: data.hastagCode,
theme: data.theme,
duration: formattedDuration,
description: data.description,
scoringFormula: data.scoringFormula,
targetParticipantTopLevel: handlePoldaPolresChange(),
targetParticipant: assignmentPurposeString,
targetOutput: selectedOutputs,
attachmentUrl: links,
};
// if (id != undefined) {
// requestData.id = id;
// }
const response = await postCreateContest(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
const id = response?.data?.data?.id;
loading();
if (imageFiles?.length == 0) {
setIsImageUploadFinish(true);
}
imageFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "1", "0");
});
if (videoFiles?.length == 0) {
setIsVideoUploadFinish(true);
}
videoFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "2", "0");
});
if (textFiles?.length == 0) {
setIsTextUploadFinish(true);
}
textFiles?.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "3", "0");
});
if (audioFiles?.length == 0) {
setIsAudioUploadFinish(true);
}
audioFiles.map(async (item: FileWithPreview, index: number) => {
await uploadResumableFile(
index,
String(id),
item, // Use .file to access the actual File object
"4",
"0" // Optional: Replace with actual duration if available
);
});
};
const onSubmit = (data: ContestSchema) => {
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 toggleExpand = (poldaId: any) => {
setExpandedPolda((prev: any) => ({
...prev,
[poldaId]: !prev[poldaId],
}));
};
const onRecordingStart = () => {
setIsRecording(true);
const countdown = setInterval(() => {
setTimer((prevTimer) => {
if (prevTimer <= 1) {
clearInterval(countdown);
return 0;
}
return prevTimer - 1;
});
}, 1000);
setTimeout(() => {
if (isRecording) {
handleStopRecording();
}
}, 120000);
};
const handleStopRecording = () => {
setIsRecording(false);
setTimer(120); // Reset the timer to 2 minutes for the next recording
};
const addAudioElement = (blob: Blob) => {
const url = URL.createObjectURL(blob);
const audio = document.createElement("audio");
audio.src = url;
audio.controls = true;
document.body.appendChild(audio);
// Convert Blob to File and add preview
const fileWithPreview: FileWithPreview = Object.assign(
new File([blob], "voiceNote.webm", { type: "audio/webm" }),
{ preview: url }
);
// Add to state
setAudioFile(fileWithPreview);
setAudioFiles((prev) => [...prev, fileWithPreview]);
};
const handleDeleteAudio = (index: number) => {
setAudioFiles((prev) => prev.filter((_, idx) => idx !== index));
};
async function uploadResumableFile(
idx: number,
id: string,
file: any,
fileTypeId: string,
duration: string
) {
console.log("Param Upload : ", 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}/contest/file/upload`,
headers: headers,
retryDelays: [0, 3000, 6000, 12_000, 24_000],
chunkSize: 20_000,
metadata: {
contestId: 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);
} else if (fileTypeId == "2") {
setIsVideoUploadFinish(true);
}
if (fileTypeId == "3") {
setIsTextUploadFinish(true);
}
if (fileTypeId == "4") {
setIsAudioUploadFinish(true);
}
},
});
upload.start();
}
useEffect(() => {
successTodo();
}, [
isImageUploadFinish,
isVideoUploadFinish,
isAudioUploadFinish,
isTextUploadFinish,
]);
function successTodo() {
if (
isImageUploadFinish &&
isVideoUploadFinish &&
isAudioUploadFinish &&
isTextUploadFinish
) {
successSubmit("/in/shared/contest");
}
}
const successSubmit = (redirect: string) => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
};
const handleLinkChange = (index: number, value: string) => {
const updatedLinks = [...links];
updatedLinks[index] = value;
setLinks(updatedLinks);
};
const handleAddRow = () => {
setLinks([...links, ""]);
};
// Remove a specific link row
const handleRemoveRow = (index: number) => {
const updatedLinks = links.filter((_: any, i: any) => i !== index);
setLinks(updatedLinks);
};
return (
<Card>
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Contest</p>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
<div className="space-y-2">
<Label>Kode Lomba</Label>
<Controller
control={control}
name="hastagCode"
render={({ field: { onChange, value } }) => (
<Input
size="md"
type="text"
value={detail?.hastagCode || value}
onChange={onChange}
placeholder="Enter hastagCode"
/>
)}
/>
{errors.hastagCode?.message && (
<p className="text-red-400 text-sm">
{errors.hastagCode.message}
</p>
)}
</div>
{/* Input Title */}
<div className="space-y-2 mt-5">
<Label>Judul Lomba</Label>
<Controller
control={control}
name="theme"
render={({ field: { onChange, value } }) => (
<Input
size="md"
type="text"
defaultValue={detail?.theme || value}
onChange={onChange}
placeholder="Enter theme"
/>
)}
/>
{errors.theme?.message && (
<p className="text-red-400 text-sm">{errors.theme.message}</p>
)}
</div>
<div className="flex flex-col mt-5">
<Label className="mr-3 mb-1">Tanggal</Label>
<Controller
control={control}
name="duration"
render={({ field: { onChange, value } }) => (
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={"outline"}
className={cn(
"w-[280px] lg:w-[300px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
value={detail?.duration}
>
<CalendarIcon />
{date?.from ? (
date.to ? (
<>
{format(date.from, "yyyy-MM-dd")} -{" "}
{format(date.to, "yyyy-MM-dd")}
</>
) : (
format(date.from, "yyyy-MM-dd")
)
) : (
<span>Pilih Tanggal</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={date?.from}
selected={date}
onSelect={(newDate) => {
setDate(newDate); // Update state lokal
if (newDate?.from) {
const formattedDate = [
format(newDate.from, "yyyy-MM-dd"),
newDate.to
? format(newDate.to, "yyyy-MM-dd")
: "",
].filter(Boolean); // Hanya menyimpan yang tidak undefined
onChange(formattedDate); // Simpan ke React Hook Form
} else {
onChange([]); // Reset jika tidak ada tanggal
}
}}
numberOfMonths={1}
/>
</PopoverContent>
</Popover>
)}
/>
{errors.duration?.message && (
<p className="text-red-400 text-sm">
{errors.duration.message}
</p>
)}
</div>
<div className="mt-5">
<Label>Output Lomba</Label>
<div className="flex flex-wrap gap-3 mt-2">
{Object.keys(taskOutput).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={taskOutput[key as keyof typeof taskOutput]}
onCheckedChange={(value) =>
setTaskOutput({ ...taskOutput, [key]: value })
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
</div>
<div className="flex flex-col mt-7">
<Label>Pelaksana Tugas</Label>
<div className="flex flex-row mt-2 gap-3">
{Object.keys(unitSelection).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={unitSelection[key as keyof typeof unitSelection]}
onCheckedChange={(value) =>
setUnitSelection({ ...unitSelection, [key]: value })
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
<div className=" pl-1">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[Kustom]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>
Daftar Wilayah Polda dan Polres
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listDest?.map((polda: any) => (
<div key={polda.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(polda.id)}
onCheckedChange={() =>
handleCheckboxChange(polda.id)
}
className="mr-3"
/>
{polda.name}
<button
onClick={() => toggleExpand(polda.id)}
className="ml-2 focus:outline-none"
>
{expandedPolda[polda.id] ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</button>
</Label>
{expandedPolda[polda.id] && (
<div className="ml-6 mt-2">
<Label className="block">
<Checkbox
checked={polda?.subDestination?.every(
(polres: any) =>
checkedLevels.has(polres.id)
)}
onCheckedChange={(isChecked) => {
const updatedLevels = new Set(
checkedLevels
);
polda?.subDestination?.forEach(
(polres: any) => {
if (isChecked) {
updatedLevels.add(polres.id);
} else {
updatedLevels.delete(polres.id);
}
}
);
setCheckedLevels(updatedLevels);
}}
className="mr-2"
/>
Pilih Semua Polres
</Label>
{polda?.subDestination?.map((polres: any) => (
<Label key={polres.id} className="block mt-1">
<Checkbox
checked={checkedLevels.has(polres.id)}
onCheckedChange={() =>
handleCheckboxChange(polres.id)
}
className="mr-2"
/>
{polres.name}
</Label>
))}
</div>
)}
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
<div className="mt-7">
<Label>Narasi Penugasan</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={onChange}
initialData={detail?.description || value}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
<div className="mt-7">
<Label>Panduan Penilaian</Label>
<Controller
control={control}
name="scoringFormula"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={onChange}
initialData={detail?.scoringFormula || value}
/>
)}
/>
{errors.scoringFormula?.message && (
<p className="text-red-400 text-sm">
{errors.scoringFormula.message}
</p>
)}
</div>
<div className="space-y-1.5 mt-5">
<Label htmlFor="attachments">Lampiran</Label>
<div className="space-y-3">
<div>
<Label>Video</Label>
<FileUploader
accept={{
"mp4/*": [],
"mov/*": [],
}}
maxSize={100}
label="Upload file dengan format .mp4 atau .mov."
onDrop={(files) => setVideoFiles(files)}
/>
</div>
<div>
<Label>Foto</Label>
<FileUploader
accept={{
"image/*": [],
}}
maxSize={100}
label="Upload file dengan format .png, .jpg, atau .jpeg."
onDrop={(files) => setImageFiles(files)}
/>
</div>
<div>
<Label>Teks</Label>
<FileUploader
accept={{
"pdf/*": [],
}}
maxSize={100}
label="Upload file dengan format .pdf."
onDrop={(files) => setTextFiles(files)}
/>
</div>
<div>
<Label>Audio</Label>
<AudioRecorder
onRecordingComplete={addAudioElement}
audioTrackConstraints={{
noiseSuppression: true,
echoCancellation: true,
}}
downloadOnSavePress={true}
downloadFileExtension="webm"
/>
<FileUploader
accept={{
"mp3/*": [],
"wav/*": [],
}}
maxSize={100}
label="Upload file dengan format .mp3 atau .wav."
onDrop={(files) =>
setAudioFiles((prev) => [...prev, ...files])
}
className="mt-2"
/>
</div>
{audioFiles?.map((audio: any, idx: any) => (
<div
key={idx}
className="flex flex-row justify-between items-center"
>
<p>Voice Note</p>
<Button
type="button"
onClick={() => handleDeleteAudio(idx)}
size="sm"
color="destructive"
>
X
</Button>
</div>
))}
{isRecording && <p>Recording... {timer} seconds remaining</p>}{" "}
{/* Display remaining time */}
<div className="mt-4">
<Label className="">Link Berita</Label>
{links.map((link, index) => (
<div key={index} className="flex items-center gap-2 mt-2">
<Input
type="url"
className="border rounded p-2 w-full"
placeholder={`Masukkan link berita ${index + 1}`}
value={link}
onChange={(e) =>
handleLinkChange(index, e.target.value)
}
/>
{links.length > 1 && (
<button
type="button"
className="bg-red-500 text-white px-3 py-1 rounded"
onClick={() => handleRemoveRow(index)}
>
<Trash2 className="h-4 w-4" />
</button>
)}
</div>
))}
<Button
type="button"
size="md"
className="mt-2 bg-blue-500 text-white px-4 py-2 rounded"
onClick={handleAddRow}
>
Tambah Link
</Button>
</div>
</div>
</div>
</div>
<div className="mt-4">
<Button type="submit" color="primary">
Submit
</Button>
</div>
</form>
</div>
</Card>
);
}

View File

@ -0,0 +1,389 @@
"use client";
import React, { useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { Switch } from "@/components/ui/switch";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { addDays, format, setDate } from "date-fns";
import { DateRange } from "react-day-picker";
import TimePicker from "react-time-picker";
import "react-time-picker/dist/TimePicker.css";
import "react-clock/dist/Clock.css";
import { register } from "module";
import MapHome from "@/components/maps/MapHome";
import { Textarea } from "@/components/ui/textarea";
import { error } from "@/lib/swal";
import Cookies from "js-cookie";
import { useRouter } from "@/i18n/routing";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "sonner";
import { close, loading } from "@/config/swal";
import { useTranslations } from "next-intl";
import { postSchedule } from "@/service/service/schedule/schedule";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul harus diisi" }),
level: z.string().min(1, { message: "Pangkat harus diisi" }),
name: z.string().min(1, { message: "Nama harus diisi" }),
location: z.string().min(1, { message: "Lokasi harus diisi" }),
});
export default function FormLiveReport() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
const [isLiveStreamingEnabled, setIsLiveStreamingEnabled] = useState(false);
type TaskSchema = z.infer<typeof taskSchema>;
const [startTime, setStartTime] = useState("08:00");
const [endTime, setEndTime] = useState("09:00");
const [scheduleTypeId, setScheduleTypeId] = React.useState<string>("1");
const [youtubeUrl, setYoutubeUrl] = useState("");
const t = useTranslations("Form");
const [date, setDate] = React.useState<DateRange | undefined>({
from: new Date(),
to: new Date(),
});
const handleStartTime = (e: any) => {
setStartTime(e.target.value);
};
const handleEndTime = (e: any) => {
setEndTime(e.target.value);
};
// State for various form fields
const [assignmentType, setAssignmentType] = useState("mediahub");
const [assignmentCategory, setAssignmentCategory] = useState("publication");
const [selectedTarget, setSelectedTarget] = useState("all");
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<TaskSchema>({
resolver: zodResolver(taskSchema),
defaultValues: {
location: "",
},
});
const save = async (data: TaskSchema) => {
const requestData = {
title: data.title,
address: data.location,
speakerTitle: data.level,
speakerName: data.name,
startTime,
endTime,
addressLat: "0.0",
addressLong: "0.0",
startDate: date?.from ? format(date.from, "yyyy-MM-dd") : null,
endDate: date?.to ? format(date.to, "yyyy-MM-dd") : null,
isYoutube: isLiveStreamingEnabled,
youtubeUrl: isLiveStreamingEnabled ? youtubeUrl : null,
scheduleTypeId: Number(scheduleTypeId),
};
console.log("Form Data Submitted:", requestData);
loading();
const response = await postSchedule(requestData);
close();
if (response?.error) {
error(response?.message);
return false;
}
Cookies.set("scheduleId", response?.data?.data.id, {
expires: 1,
});
// Optional: Use Swal for success feedback
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/contributor/schedule/live-report");
});
};
const onSubmit = (data: TaskSchema) => {
if (isLiveStreamingEnabled && youtubeUrl == "") {
toast(<p className="text-red-600">Youtube ID harus diisi</p>);
return false;
}
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);
}
});
};
return (
<div className="flex flex-col lg:flex-row gap-2 border rounded-lg ">
<Card className="w-full bg-slate-100">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Konferensi Pers</p>
<form onSubmit={handleSubmit(onSubmit)}>
<div className=" gap-5 mb-5">
{/* Input Title */}
<div className="space-y-2">
<Label>Judul Kegiatan</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size={"md"}
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Masukan Judul"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="flex flex-row items-center">
<div className="mt-4 mb-3">
<Label>Live Streaming</Label>
<div className="flex items-center gap-3">
<Label>Aktifkan fitur live streaming</Label>
<Switch
className="border border-slate-100 bg-red-100"
defaultChecked={isLiveStreamingEnabled}
color="primary"
id="c2"
onCheckedChange={(checked) =>
setIsLiveStreamingEnabled(checked)
}
/>
</div>
</div>
</div>
{isLiveStreamingEnabled && (
<div className="mb-2 mt-1">
<Input
size={"md"}
type="text"
value={youtubeUrl}
onChange={(e) => setYoutubeUrl(e.target.value)}
placeholder="Masukan Youtube ID"
/>
</div>
)}
<div className="flex flex-col space-y-2">
<Label className="mr-3 mb-1">Tipe</Label>
<Select onValueChange={(value) => setScheduleTypeId(value)}>
<SelectTrigger className="w-full ">
<SelectValue placeholder="Masukan Tipe" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Tipe</SelectLabel>
<SelectItem value="1">Konferensi Pers</SelectItem>
<SelectItem value="2">Event</SelectItem>
<SelectItem value="3">Pers Rilis</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="flex flex-col lg:flex-row mb-4 mt-2 items-start lg:items-center justify-between">
<div className="flex flex-col space-y-2">
<Label className="mr-3 mb-1">{t("date")}</Label>
<Popover>
<PopoverTrigger asChild className="px-0">
<Button
size="md"
id="date"
variant={"outline"}
className={cn(
"w-[280px] lg:w-[250px] justify-start text-left font-normal border border-slate-300 px-0 md:px-0 lg:px-4",
!date && "text-muted-foreground"
)}
>
<CalendarIcon size={15} className="mr-3" />
{date?.from ? (
date.to ? (
<>
{format(date.from, "LLL dd, y")} -{" "}
{format(date.to, "LLL dd, y")}
</>
) : (
format(date.from, "LLL dd, y")
)
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={date?.from}
selected={date}
onSelect={setDate}
numberOfMonths={1}
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="title">Rentang Waktu</Label>
<div className="">
<div className="flex flex-row items-center">
<div className="col-6">
<Input
defaultValue="08:00"
type="time"
onChange={(e) => handleStartTime(e)}
/>
</div>
<div className="col-6">
<Input
defaultValue="09:00"
type="time"
onChange={(e) => handleEndTime(e)}
/>
</div>
</div>
</div>
</div>
</div>
<div>
{/* Kirim setValue ke MapHome */}
<MapHome
draggable
setLocation={(location) => setValue("location", location)}
/>
</div>
<div>
<Controller
control={control}
name="location"
render={({ field }) => (
<Textarea
rows={3}
value={field.value}
onChange={field.onChange}
placeholder="Masukan lokasi"
/>
)}
/>
{errors.location?.message && (
<p className="text-red-400 text-sm">
{errors.location.message}
</p>
)}
</div>
<p className="text-sm mt-4 font-semibold">DI SAMPAIKAN OLEH</p>
<div className="flex flex-col ">
<div className="mt-1 space-y-2">
<Label>Nama Pangkat</Label>
<Controller
control={control}
name="level"
render={({ field }) => (
<Input
size={"md"}
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Masukan Nama Pangkat"
/>
)}
/>
{errors.level?.message && (
<p className="text-red-400 text-sm">
{errors.level.message}
</p>
)}
</div>
</div>
<div className="flex flex-col my-3">
<div className="mt-1 space-y-2">
<Label>Nama Lengkap</Label>
<Controller
control={control}
name="name"
render={({ field }) => (
<Input
size={"md"}
type="text"
value={field.value}
onChange={field.onChange}
placeholder="Masukan Nama Lengkap"
/>
)}
/>
{errors.name?.message && (
<p className="text-red-400 text-sm">
{errors.name.message}
</p>
)}
</div>
</div>
</div>
{/* Submit Button */}
<div className="mt-4">
<Button type="submit" color="primary">
Submit
</Button>
</div>
</form>
</div>
</Card>
{/* <Card className="w-full lg:w-3/12">
<div className="px-3 py-3">Jadwal Selanjutnya</div>
</Card> */}
</div>
);
}

View File

@ -0,0 +1,131 @@
"use client";
import { Fragment, useState } from "react";
import { Icon } from "@iconify/react";
import { Accept, useDropzone } from "react-dropzone";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { CloudUpload } from "lucide-react";
interface FileWithPreview extends File {
preview: string;
}
interface FileUploaderProps {
onDrop: (files: FileWithPreview[]) => void;
accept: Accept;
maxSize: number;
label: string;
className?: string;
isMultiple?: boolean;
}
const FileUploader = ({
onDrop,
accept,
maxSize,
label,
className = "",
isMultiple = true,
}: FileUploaderProps) => {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const { getRootProps, getInputProps } = useDropzone({
accept: accept,
maxSize: maxSize * 1024 * 1024,
onDrop: (acceptedFiles) => {
const mappedFiles = acceptedFiles.map((file) => Object.assign(file));
setFiles((prevFiles) => [...prevFiles, ...mappedFiles]);
onDrop(mappedFiles);
},
});
const renderFilePreview = (file: FileWithPreview) => {
if (file.type.startsWith("image")) {
return (
<Image
width={48}
height={48}
alt={file.name}
src={URL.createObjectURL(file)}
className=" rounded border p-0.5"
/>
);
} else {
return <Icon icon="tabler:file-description" />;
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const handleRemoveAllFiles = () => {
setFiles([]);
onDrop([]);
};
const fileList = files.map((file) => (
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 my-6 rounded-md"
>
<div className="flex gap-3 items-center">
<div className="file-preview">{renderFilePreview(file)}</div>
<div>
<div className=" text-sm text-card-foreground">{file.name}</div>
<div className=" text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
</div>
</div>
<Button
size="icon"
color="destructive"
variant="outline"
className=" border-none rounded-full"
onClick={() => handleRemoveFile(file)}
>
<Icon icon="tabler:x" className=" h-5 w-5" />
</Button>
</div>
));
return (
<Fragment>
<div {...getRootProps({ className: "dropzone" })} className={className}>
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
{/* Drop files here or click to upload. */}
Tarik file disini atau klik untuk upload.
</h4>
<div className=" text-xs text-muted-foreground">
( {label}
Ukuran maksimal {maxSize} MB.)
</div>
</div>
</div>
{files.length ? (
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
<Button color="destructive" onClick={handleRemoveAllFiles}>
Remove All
</Button>
</div>
</Fragment>
) : null}
</Fragment>
);
};
export default FileUploader;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,925 @@
"use client";
import React, { useEffect, useRef, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
createAssignmentResponse,
createTask,
deleteAssignmentResponse,
deleteTask,
finishTask,
forwardTask,
getAcceptance,
getAssignmentResponseList,
getTask,
getUserLevelForAssignments,
} from "@/service/task";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ChevronDown, ChevronUp, DotSquare, TrashIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { Link } from "@/components/navigation";
import { Textarea } from "@/components/ui/textarea";
import { close, error, loading } from "@/lib/swal";
import { getCookiesDecrypt } from "@/lib/utils";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
const taskSchema = z.object({
// uniqueCode: z.string().min(1, { message: "Judul diperlukan" }),
title: z.string().min(1, { message: "Judul diperlukan" }),
naration: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
forwardMessage: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type taskDetail = {
id: number;
uniqueCode: string;
title: string;
fileTypeOutput: string;
assignedToRole: string;
assignedToTopLevel: string;
assignmentType: {
id: number;
name: string;
};
assignmentMainType: {
id: number;
name: string;
};
createdBy: {
id: number;
fullname: string;
username: string | null;
email: string;
isActive: boolean;
isDefault: boolean;
isInternational: boolean;
userLevel: {
id: number;
name: string;
aliasName: string;
userGroupId: number;
};
};
taskType: string;
broadcastType: string;
narration: string;
is_active: string;
isDone: any;
};
interface ListData {
id: number;
name: string;
content: string;
timestamp: string;
}
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false }
);
const formatDate = (dateString: string): string => {
const date = new Date(dateString);
// Pastikan validitas tanggal
if (isNaN(date.getTime())) {
throw new Error("Invalid date format");
}
// Format tanggal
const day = date.getDate().toString().padStart(2, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const year = date.getFullYear();
// const hours = date.getHours().toString().padStart(2, "0");
// Gabungkan hasil format
return `${day}-${month}-${year} `;
};
interface AcceptanceData {
id: number;
acceptAt: string;
// Tambahkan properti lain sesuai dengan struktur data Anda
}
interface AcceptanceData {
id: number;
userLevelId: number;
sentAt: string;
isAccept: boolean;
isSent: boolean;
userLevels: {
id: number;
name: string;
aliasName: string;
};
}
export default function FormTaskForward() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
type TaskSchema = z.infer<typeof taskSchema>;
const { id } = useParams() as { id: string };
console.log(id);
const userLevelNumber = getCookiesDecrypt("ulne");
const userId = getCookiesDecrypt("uie");
// State for various form fields
const [taskOutput, setTaskOutput] = useState({
all: false,
video: false,
audio: false,
image: false,
text: false,
});
// const [assignmentType, setAssignmentType] = useState("mediahub");
// const [assignmentCategory, setAssignmentCategory] = useState("publication");
const [mainType, setMainType] = useState<string>("1");
const [taskType, setTaskType] = useState<string>("atensi-khusus");
const [broadcastType, setBroadcastType] = useState<string>(""); // untuk Tipe Penugasan
const [type, setType] = useState<string>("1");
const [selectedTarget, setSelectedTarget] = useState("all");
const [detail, setDetail] = useState<taskDetail>();
const [refresh] = useState(false);
const [listDest, setListDest] = useState([]); // Data Polda dan Polres
const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false);
const [responses, setResponses] = useState<Response[]>([]);
const [response, setResponse] = useState<string>("");
const [showInput, setShowInput] = useState<boolean>(false);
const [listData, setListData] = useState([]);
const [message, setMessage] = useState<string>("");
const [sentAcceptance, setSentAcceptance] = useState<AcceptanceData[]>([]);
const [acceptAcceptance, setAcceptAcceptance] = useState<AcceptanceData[]>(
[]
);
const [modalType, setModalType] = useState<"terkirim" | "diterima" | "">("");
const [Isloading, setLoading] = useState<boolean>(false);
const [refreshAcceptance, setRefreshAcceptance] = useState<boolean>(false);
const [replyingTo, setReplyingTo] = useState<number | null>(null);
const [forwardMessage, setForwardMessage] = useState();
const [narration, setNarration] = useState(null);
const [checkedLevels, setCheckedLevels] = useState(new Set());
const [platformTypeVisible, setPlatformTypeVisible] = useState(false);
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
const {
control,
register,
handleSubmit,
formState: { errors },
} = useForm<TaskSchema>({
resolver: zodResolver(taskSchema),
});
// const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// const selectedValue = Number(event.target.value);
// setMainType(selectedValue);
// setPlatformTypeVisible(selectedValue === 2);
// };
useEffect(() => {
async function fetchPoldaPolres() {
setIsLoading(true);
try {
const response = await getUserLevelForAssignments();
setListDest(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();
}, []);
useEffect(() => {
async function initState() {
if (id) {
const response = await getTask(id);
const details = response?.data?.data;
setDetail(details);
if (details?.assignedToLevel) {
const levels = new Set(
details.assignedToLevel.split(",").map(Number)
);
setCheckedLevels(levels);
}
}
}
initState();
}, [id, refresh]);
useEffect(() => {
if (detail?.broadcastType) {
setBroadcastType(detail.broadcastType); // Mengatur nilai broadcastType dari API
}
}, [detail?.broadcastType]);
useEffect(() => {
if (detail?.fileTypeOutput) {
const outputSet = new Set(detail.fileTypeOutput.split(",").map(Number)); // Membagi string ke dalam array dan mengonversi ke nomor
setTaskOutput({
all: outputSet.has(0),
video: outputSet.has(2),
audio: outputSet.has(4),
image: outputSet.has(1),
text: outputSet.has(3),
});
}
}, [detail?.fileTypeOutput]);
useEffect(() => {
if (detail?.assignedToTopLevel) {
const outputSet = new Set(
detail.assignedToTopLevel.split(",").map(Number)
);
setUnitSelection({
allUnit: outputSet.has(0),
mabes: outputSet.has(1),
polda: outputSet.has(2),
polres: outputSet.has(3),
});
}
}, [detail?.fileTypeOutput]);
const save = async (data: TaskSchema) => {
const fileTypeMapping = {
all: "1",
video: "2",
audio: "3",
image: "4",
text: "5",
};
const selectedOutputs = Object.keys(taskOutput)
.filter((key) => taskOutput[key as keyof typeof taskOutput])
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping])
.join(",");
const requestData = {
...data,
// assignmentType,
// assignmentCategory,
target: selectedTarget,
unitSelection,
assignedToRole: "3",
taskType: taskType,
broadcastType: broadcastType,
assignmentMainTypeId: mainType,
assignmentPurpose: "1",
assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
id: null,
narration: data.naration,
platformType: "",
title: data.title,
};
const response = await createTask(requestData);
console.log("Form Data Submitted:", requestData);
console.log("response", response);
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/en/contributor/task");
});
};
const onSubmit = (data: TaskSchema) => {
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 successConfirm = () => {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/en/contributor/task");
}
});
};
const handlePoldaPolresChange = () => {
return Array.from(checkedLevels).join(",");
};
async function saveForward() {
console.log("Narasi :", narration);
loading();
const data = {
id,
forwardMessage,
assignedToLevel: handlePoldaPolresChange(),
};
const response = await forwardTask(data);
if (response?.error) {
error(response.message);
return false;
}
successConfirm();
return false;
}
const handleCheckboxChange = (levelId: any) => {
setCheckedLevels((prev: any) => {
const updatedLevels = new Set(prev);
if (updatedLevels.has(levelId)) {
updatedLevels.delete(levelId);
} else {
updatedLevels.add(levelId);
}
return updatedLevels;
});
};
const toggleExpand = (poldaId: any) => {
setExpandedPolda((prev: any) => ({
...prev,
[poldaId]: !prev[poldaId],
}));
};
useEffect(() => {
async function initState() {
// loading();
const response = await getAssignmentResponseList(id);
console.log("data", response?.data?.data);
console.log("userLvl", userLevelNumber);
setListData(response?.data?.data);
close();
}
initState();
}, []);
const handleToggleInput = (): void => {
setShowInput(!showInput);
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
};
const postData = () => {
sendSuggestionParent();
};
const sendReplyData = async (parentId: number) => {
const inputElement = document.querySelector(
`#input-comment-${parentId}`
) as HTMLTextAreaElement;
if (inputElement?.value?.length > 1) {
loading();
const data = {
assignmentId: id,
message: inputElement.value,
parentId,
};
console.log(data);
const response = await createAssignmentResponse(data);
console.log(response);
const responseGet = await getAssignmentResponseList(id);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
inputElement.value = "";
close();
setReplyingTo(null);
}
};
async function sendSuggestionParent() {
if (message?.length > 1) {
loading();
const data = {
assignmentId: id,
message,
parentId: null,
};
const response = await createAssignmentResponse(data);
console.log(response);
setMessage("");
const responseGet = await getAssignmentResponseList(id);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
}
async function deleteDataSuggestion(dataId: any) {
loading();
const response = await deleteAssignmentResponse(dataId);
console.log(response);
const responseGet = await getAssignmentResponseList(id);
console.log(responseGet?.data?.data);
setListData(responseGet?.data?.data);
close();
}
const deleteData = (dataId: any) => {
deleteDataSuggestion(dataId);
console.log(dataId);
};
async function getListAcceptance(): Promise<void> {
const isAccept = true;
try {
const resSent = await getAcceptance(id, !isAccept);
setSentAcceptance(resSent?.data?.data);
const resAccept = await getAcceptance(id, isAccept);
const acceptanceSort = resAccept?.data?.data?.sort(
(a: AcceptanceData, b: AcceptanceData) =>
new Date(a.acceptAt).getTime() - new Date(b.acceptAt).getTime()
);
console.log("Data sort:", acceptanceSort);
setAcceptAcceptance(acceptanceSort);
} catch (error) {
console.error("Error fetching acceptance data:", error);
}
}
useEffect(() => {
async function initState(): Promise<void> {
await getListAcceptance();
}
initState();
}, [refreshAcceptance]);
const setFinishAcceptance = async (
id: number,
isFinish: boolean
): Promise<void> => {
if (!isFinish) {
loading();
setRefreshAcceptance((prev) => !prev);
close();
}
};
const handleReply = (id: any) => {
setReplyingTo(id);
};
async function finishAssignment() {
const response = finishTask(id);
// if (response.error) {
// error(response.message);
// return false;
// }
successConfirm();
}
async function deleteAssignment() {
const response = deleteTask(id);
// if (response.error) {
// error(response.message);
// return false;
// }
successConfirm();
}
async function handleForward() {
MySwal.fire({
title: "Forward Penugasan?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Ya",
cancelButtonText: "Tidak",
}).then((result) => {
if (result.isConfirmed) {
saveForward();
}
});
}
async function handleDeleteAssignment() {
MySwal.fire({
title: "Apakah Anda yakin ingin menghapus data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
deleteAssignment();
}
});
}
async function handleAssignmentDone() {
MySwal.fire({
title: "Apakah tugas sudah selesai?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Ya",
cancelButtonText: "Tidak",
}).then((result) => {
if (result.isConfirmed) {
finishAssignment();
}
});
}
return (
<Card>
{detail !== undefined ? (
<div className="px-6 py-6">
<div className="flex flex-row justify-between">
<p className="text-lg font-semibold mb-3">Tujuan Forward</p>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listDest.map((polda: any) => (
<div key={polda.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(polda.id)}
onCheckedChange={() => handleCheckboxChange(polda.id)}
className="mr-3"
/>
{polda.name}
<button
onClick={() => toggleExpand(polda.id)}
className="ml-2 focus:outline-none"
></button>
</Label>
</div>
))}
</div>
<div className="form-group mt-2">
<Label htmlFor="message">Pesan</Label>
<Textarea
id="forwardMessage"
placeholder="Message"
{...register("forwardMessage")}
/>
</div>
<Button
color="primary"
variant={"default"}
type="button"
className="btn btn-primary mr-2 float-right my-3"
onClick={() => handleForward()}
>
Forward Tugas
</Button>
<div className="space-y-2 mt-6">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
size="md"
type="text"
value={detail?.title}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{errors.title?.message && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
<div className="flex flex-row items-center">
<div className="mt-6">
<Label>Tujuan Pemilihan Tugas</Label>
<Select
onValueChange={setSelectedTarget}
value={detail.assignedToRole}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="3,4">Semua Pengguna</SelectItem>
<SelectItem value="4">Kontributor</SelectItem>
<SelectItem value="3">Approver</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap gap-3 mt-6 pt-5 ml-3">
{Object.keys(unitSelection).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
disabled
checked={
unitSelection[key as keyof typeof unitSelection]
}
onCheckedChange={(value) =>
setUnitSelection({ ...unitSelection, [key]: value })
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
<div className="mt-6 pt-5 pl-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[Kustom]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>
Daftar Wilayah Polda dan Polres
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listDest.map((polda: any) => (
<div key={polda.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
disabled
checked={checkedLevels.has(polda.id)}
onCheckedChange={() =>
handleCheckboxChange(polda.id)
}
className="mr-3"
/>
{polda.name}
<button
onClick={() => toggleExpand(polda.id)}
className="ml-2 focus:outline-none"
>
{expandedPolda[polda.id] ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
</button>
</Label>
{expandedPolda[polda.id] && (
<div className="ml-6 mt-2">
<Label className="block">
<Checkbox
disabled
checked={polda?.subDestination?.every(
(polres: any) =>
checkedLevels.has(polres.id)
)}
onCheckedChange={(isChecked) => {
const updatedLevels = new Set(
checkedLevels
);
polda?.subDestination?.forEach(
(polres: any) => {
if (isChecked) {
updatedLevels.add(polres.id);
} else {
updatedLevels.delete(polres.id);
}
}
);
setCheckedLevels(updatedLevels);
}}
className="mr-2"
/>
Pilih Semua Polres
</Label>
{polda?.subDestination?.map((polres: any) => (
<Label key={polres.id} className="block mt-1">
<Checkbox
disabled
checked={checkedLevels.has(polres.id)}
onCheckedChange={() =>
handleCheckboxChange(polres.id)
}
className="mr-2"
/>
{polres.name}
</Label>
))}
</div>
)}
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
</div>
<div className="mt-6">
<Label>Tipe Penugasan</Label>
<RadioGroup
value={detail.assignmentMainType.id.toString()}
onValueChange={(value) => setMainType(value)}
// value={String(mainType)}
// onValueChange={(value) => setMainType(Number(value))}
className="flex flex-wrap gap-3"
>
<RadioGroupItem value="1" id="mediahub" />
<Label htmlFor="mediahub">Mediahub</Label>
<RadioGroupItem value="2" id="medsos-mediahub" />
<Label htmlFor="medsos-mediahub">Medsos Mediahub</Label>
</RadioGroup>
</div>
<div className="mt-6">
<Label>Jenis Tugas </Label>
<RadioGroup
value={detail.taskType.toString()}
onValueChange={(value) => setTaskType(String(value))}
className="flex flex-wrap gap-3"
>
<RadioGroupItem value="atensi-khusus" id="khusus" />
<Label htmlFor="atensi-khusus">Atensi Khusus</Label>
<RadioGroupItem value="tugas-harian" id="harian" />
<Label htmlFor="tugas-harian">Tugas Harian</Label>
</RadioGroup>
</div>
{/* RadioGroup Assignment Category */}
<div className="mt-6">
<Label>Jenis Penugasan</Label>
<RadioGroup
value={detail.assignmentType.id.toString()}
onValueChange={(value) => setType(value)}
className="flex flex-wrap gap-3"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="1" id="publication" />
<Label htmlFor="publication">Publikasi</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="2" id="amplification" />
<Label htmlFor="amplification">Amplifikasi</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="3" id="contra" />
<Label htmlFor="contra">Kontra</Label>
</div>
</RadioGroup>
</div>
<div className="mt-6">
<Label>Output Tugas</Label>
<div className="flex flex-wrap gap-3">
{Object.keys(taskOutput).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
disabled
checked={taskOutput[key as keyof typeof taskOutput]}
onCheckedChange={(value) =>
setTaskOutput({ ...taskOutput, [key]: value })
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
</div>
{/* <div className="mt-6">
<Label>Broadcast </Label>
<RadioGroup
value={broadcastType}
onValueChange={(value) => setBroadcastType(value)}
className="flex flex-wrap gap-3"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="all" id="all" />
<Label htmlFor="all">Semua</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="email" id="email" />
<Label htmlFor="email">Email Blast</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="whatsapp" id="whatsapp" />
<Label htmlFor="whatsapp">WhatsApp Blast</Label>
</div>
</RadioGroup>
</div> */}
<div className="mt-6">
<Label>Narasi Penugasan</Label>
<Controller
control={control}
name="naration"
render={({ field: { onChange, value } }) => (
<ViewEditor initialData={detail?.narration} />
)}
/>
{errors.naration?.message && (
<p className="text-red-400 text-sm">
{errors.naration.message}
</p>
)}
</div>
</div>
</form>
</div>
) : (
""
)}
</Card>
);
}

View File

@ -0,0 +1,30 @@
export type DetailTicket = {
title: string;
description: string;
commentFromUserId: string;
assignedTeams: string;
message: string;
typeId: number;
feedId: any;
commentId: string;
priority: {
name: string;
};
status: {
name: string;
};
emergencyIssue: {
date: string;
location: string;
title: string;
urgencyName: string;
feedUrl: string;
recommendationName: string;
link: string;
uploadFiles?: {
fileUrl: string;
fileName: string;
}[];
description: string;
};
};

View File

@ -0,0 +1,137 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DetailTicket } from "./info-lainnya-types";
import { useState } from "react";
interface InfoLainnyaModalProps {
open: boolean;
onClose: () => void;
data: DetailTicket["emergencyIssue"];
}
export default function InfoLainnyaModal({
open,
onClose,
data,
}: InfoLainnyaModalProps) {
const files = data?.uploadFiles || [];
const [currentIndex, setCurrentIndex] = useState(0);
const handlePrev = () => {
setCurrentIndex((prev) => (prev > 0 ? prev - 1 : prev));
};
const handleNext = () => {
setCurrentIndex((prev) => (prev < files.length - 1 ? prev + 1 : prev));
};
const currentFile = files[currentIndex];
const isImage = (fileUrl: string) =>
/\.(jpeg|jpg|png|gif|bmp|webp)$/i.test(fileUrl.toLowerCase());
const getIframeUrl = (fileUrl: string): string => {
const lower = fileUrl.toLowerCase();
// Dokumen ditampilkan melalui Google Docs Viewer
if (/\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$/i.test(lower)) {
return `https://docs.google.com/viewer?url=${encodeURIComponent(
fileUrl
)}&embedded=true`;
}
return fileUrl;
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent size="md">
<DialogHeader>
<DialogTitle>Info Lainnya</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="font-medium">Tanggal</div>
<div>:{data?.date}</div>
<div className="font-medium">Lokasi Kejadian</div>
<div>:{data?.location}</div>
<div className="font-medium">Isu Menonjol</div>
<div>:{data?.title}</div>
<div className="font-medium">Urgensi</div>
<div>:{data?.urgencyName}</div>
<div className="font-medium">Rekomendasi Tindak Lanjut</div>
<div>:{data?.recommendationName}</div>
<div className="font-medium">Link Pendukung</div>
<div className="flex">
:
<a
href={data?.link}
className="text-blue-600 flex w-[100px]"
target="_blank"
rel="noopener noreferrer"
>
{data?.link}
</a>
</div>
{files.length > 0 && (
<>
<div className="font-medium col-span-2">Lampiran</div>
<div className="col-span-2 flex flex-col items-center space-y-2 w-full">
{isImage(currentFile?.fileUrl || "") ? (
<img
src={currentFile?.fileUrl}
alt={`Lampiran ${currentIndex + 1}`}
className="max-h-[300px] w-auto object-contain border rounded"
/>
) : (
// <iframe
// src={getIframeUrl(currentFile?.fileUrl || "")}
// title={`Lampiran ${currentIndex + 1}`}
// className="w-full max-w-2xl h-[300px] border rounded"
// onError={(e) => {
// (e.target as HTMLIFrameElement).style.display = "none";
// }}
// />
<iframe
src={getIframeUrl(currentFile?.fileUrl || "")}
title={`Lampiran ${currentIndex + 1}`}
className="w-full max-w-2xl h-[500px] rounded overflow-hidden"
scrolling="no"
onError={(e) => {
(e.target as HTMLIFrameElement).style.display = "none";
}}
/>
)}
<div className="flex gap-2">
<button
onClick={handlePrev}
disabled={currentIndex === 0}
className="px-2 py-1 border rounded disabled:opacity-50"
>
&lt;
</button>
<button
onClick={handleNext}
disabled={currentIndex === files.length - 1}
className="px-2 py-1 border rounded disabled:opacity-50"
>
&gt;
</button>
</div>
</div>
</>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,817 @@
// "use client";
// import React, { useEffect, useState } from "react";
// import { useForm, Controller } from "react-hook-form";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import { Label } from "@/components/ui/label";
// import { Card } from "@/components/ui/card";
// import { zodResolver } from "@hookform/resolvers/zod";
// import * as z from "zod";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// import { useParams } from "next/navigation";
// import {
// Select,
// SelectContent,
// SelectItem,
// SelectTrigger,
// SelectValue,
// } from "@/components/ui/select";
// import { Avatar, AvatarImage } from "@/components/ui/avatar";
// import {
// deleteTicket,
// getTicketingDetail,
// getTicketingInternalDetail,
// getTicketingInternalDiscussion,
// getTicketingReply,
// saveTicketing,
// saveTicketInternalReply,
// saveTicketReply,
// } from "@/service/communication/communication";
// import { Icon } from "@iconify/react/dist/iconify.js";
// import { list, parse } from "postcss";
// import { htmlToString } from "@/utils/globals";
// import { Textarea } from "@/components/ui/textarea";
// import { error } from "@/lib/swal";
// import { useRouter } from "next/navigation";
// import InfoLainnyaModal from "./info-lainnya";
// import { DetailTicket } from "./info-lainnya-types";
// import { useMediaQuery } from "react-responsive";
// import { ArrowLeft, ChevronDownIcon } from "lucide-react";
// import {
// Popover,
// PopoverContent,
// PopoverTrigger,
// } from "@/components/ui/popover";
// import { getOperatorUser } from "@/service/management-user/management-user";
// const taskSchema = z.object({
// title: z.string().min(1, { message: "Judul diperlukan" }),
// naration: z.string().min(2, {
// message: "Narasi Penugasan harus lebih dari 2 karakter.",
// }),
// });
// export type taskDetail = {
// id: number;
// title: string;
// createdAt: string;
// referenceNumber: string | number;
// createdBy: {
// id: number;
// fullname: string;
// };
// sendTo: {
// id: number;
// fullname: string;
// };
// status: {
// id: number;
// name: string;
// };
// priority: {
// id: number;
// name: string;
// };
// broadcastType: string;
// description: string;
// is_active: string;
// };
// export type replyDetail = {
// id: number;
// comments: string;
// createdAt: string;
// user: {
// id: number;
// fullname: string;
// };
// messageTo: {
// id: number;
// fullname: string;
// };
// };
// export type internalDetail = {
// id: number;
// message: string;
// createdAt: string;
// commentFromUserName: string;
// feedTitle: string;
// createdBy: {
// id: number;
// fullname: string;
// };
// sendTo: {
// id: number;
// fullname: string;
// };
// };
// type Props = { id: string };
// export default function FormDetailTicketing({ id }: Props) {
// const MySwal = withReactContent(Swal);
// // const { id } = useParams() as { id: string };
// const router = useRouter();
// const isMobile = useMediaQuery({
// maxWidth: 768,
// });
// const [detail, setDetail] = useState<taskDetail>();
// const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
// const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
// null
// );
// const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
// const [replyVisible, setReplyVisible] = useState(false);
// const [replyMessage, setReplyMessage] = useState("");
// const [selectedPriority, setSelectedPriority] = useState("");
// const [selectedStatus, setSelectedStatus] = useState("");
// const [openEmergencyModal, setOpenEmergencyModal] = useState(false);
// const [replyValue, setReplyValue] = useState<number>(0);
// const [replyText, setReplyText] = useState<string>("");
// const [operatorOpt, setOperatorOpt] = useState<
// { id: string; label: string; value: string }[]
// >([]);
// const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
// const {
// control,
// handleSubmit,
// formState: { errors },
// } = useForm({
// resolver: zodResolver(taskSchema),
// });
// useEffect(() => {
// async function initState() {
// setReplyValue(0);
// const response = await getTicketingDetail(id);
// setTicketInternal(response?.data?.data || null);
// setDetail(response?.data?.data);
// if (response?.data !== null) {
// setDetailTickets(response?.data?.data);
// }
// }
// initState();
// getTicketReply();
// }, [id]);
// useEffect(() => {
// async function getOperator() {
// const res = await getOperatorUser(detailTickets?.typeId);
// if (res?.data !== null) {
// const rawUser = res?.data?.data;
// const optionArr = rawUser?.map((option: any) => ({
// id: option.id,
// label: option.fullName,
// value: option.id.toString(),
// }));
// setOperatorOpt(optionArr);
// if (detailTickets?.assignedTeams) {
// const assigned = detailTickets.assignedTeams
// .split(":")
// .filter((id: string) => id);
// setSelectedOperator(assigned);
// }
// }
// }
// getOperator();
// }, [detailTickets]);
// const handleReply = () => {
// setReplyValue((prev) => (prev === 1 ? 0 : 1));
// };
// const handleSendReplyData = async () => {
// if (!replyText.trim()) {
// console.warn("Balasan kosong!");
// return;
// }
// try {
// const res = await saveTicketReply({
// ticketId: id,
// comment: replyText,
// parentCommentId: detailTickets?.commentId,
// isFromInternal: true,
// });
// console.log("Berhasil kirim balasan:", res?.data);
// setReplyText("");
// setReplyValue(0);
// getTicketReply();
// } catch (err) {
// console.error("Gagal kirim balasan:", err);
// }
// };
// async function getTicketReply() {
// const res = await getTicketingReply(id);
// if (res?.data !== null) {
// setTicketReply(res?.data?.data);
// }
// }
// const handleSendReply = async () => {
// if (replyMessage.trim() === "") {
// MySwal.fire({
// title: "Error",
// text: "Pesan tidak boleh kosong!",
// icon: "error",
// });
// return;
// }
// const data = {
// ticketId: id,
// comment: replyMessage,
// };
// try {
// const response = await saveTicketReply(data);
// const newReply: replyDetail = {
// id: response?.data?.id,
// comments: replyMessage,
// createdAt: response?.data?.createdAt,
// user: response?.data?.messageFrom,
// messageTo: response?.data?.messageTo,
// };
// setTicketReply((prevReplies) => [newReply, ...prevReplies]);
// MySwal.fire({
// title: "Sukses",
// text: "Pesan berhasil dikirim.",
// icon: "success",
// });
// setReplyMessage("");
// setReplyVisible(false);
// } catch (error) {
// MySwal.fire({
// title: "Error",
// text: "Gagal mengirim balasan.",
// icon: "error",
// });
// console.error("Error sending reply:", error);
// }
// };
// async function doDelete(id: any) {
// const response = await deleteTicket(id);
// if (response?.error) {
// error(response.message);
// return false;
// }
// success("/in/supervisor/ticketing");
// }
// function success(redirect: string) {
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then(() => {
// router.push(redirect);
// });
// }
// const handleDelete = (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);
// }
// });
// };
// const openEmergencyIssueDetail = () => {
// setOpenEmergencyModal(true);
// };
// return (
// <div className="py-5">
// <div className="mt-4">
// <div className="flex gap-3">
// <Button onClick={handleReply} variant="outline">
// <ArrowLeft className="mr-2 h-4 w-4" />
// {replyValue === 1 ? "Tutup Balasan" : "Balas"}
// </Button>
// </div>
// {replyValue === 1 && (
// <div className="mt-4 rounded-xl bg-gray-100 p-4">
// <textarea
// value={replyText}
// onChange={(e) => setReplyText(e.target.value)}
// placeholder="Tulis Pesan"
// className="w-full resize-none rounded-md border border-gray-300 bg-white p-4 text-sm text-gray-700 focus:outline-none"
// rows={5}
// />
// <div className="mt-4 flex justify-end gap-3">
// <Button
// onClick={() => setReplyValue(0)}
// variant="outline"
// className="text-blue-600 border-blue-600"
// >
// Batal
// </Button>
// <Button
// onClick={handleSendReplyData}
// className="bg-blue-600 text-white hover:bg-blue-700"
// >
// Kirim
// </Button>
// </div>
// </div>
// )}
// </div>
// <div className="flex flex-col md:flex-row lg:flex-row gap-5 mt-5">
// <div className="flex flex-col w-[100%] lg:w-[70%]">
// {replyVisible && (
// <div className="">
// <textarea
// id="replyMessage"
// className="w-full h-24 border rounded-md p-2"
// value={replyMessage}
// onChange={(e) => setReplyMessage(e.target.value)}
// placeholder="Tulis pesan di sini..."
// />
// <div className="flex justify-end gap-3 my-2">
// <Button
// onClick={() => setReplyVisible(false)}
// color="default"
// variant="outline"
// >
// Batal
// </Button>
// <Button onClick={handleSendReply} color="primary">
// Kirim
// </Button>
// </div>
// </div>
// )}
// <div className="border rounded-t-xl">
// <p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
// Ticket #{detail?.referenceNumber}
// </p>
// {ticketReply?.map((list) => (
// <div key={list.id} className="flex flex-col mb-4">
// {isMobile ? (
// <div className="flex gap-3 bg-sky-100 p-3 items-center">
// <Icon icon="qlementine-icons:user-16" width={36} />
// <div>
// <p className="text-sm">
// <span className="font-bold">
// {list?.user?.fullname}
// </span>{" "}
// mengirimkan balasan{" "}
// <span className="font-bold">
// {list?.messageTo?.fullname}
// </span>
// </p>
// <p className="text-xs">
// {new Date(list.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "2-digit",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// })}
// </p>
// </div>
// </div>
// ) : (
// <div className="flex gap-4 bg-sky-100 p-4 items-center">
// <div className="text-center">
// <Icon icon="qlementine-icons:user-16" width={50} />
// </div>
// <div>
// <p className="text-sm">
// <span className="font-bold">
// {list?.user?.fullname}
// </span>{" "}
// mengirimkan balasan{" "}
// <span className="font-bold">
// {list?.messageTo?.fullname}
// </span>
// </p>
// <p className="text-xs">
// {new Date(list.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "2-digit",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// })}
// </p>
// </div>
// </div>
// )}
// <div className="bg-white text-sm p-4">{list.comments}</div>
// </div>
// ))}
// {ticketInternal && (
// <div key={ticketInternal.id} className="flex flex-col">
// <div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
// <Icon icon="qlementine-icons:user-16" width={36} />
// <div>
// <p>
// <span className="font-bold text-sm">
// {ticketInternal?.commentFromUserName}
// </span>{" "}
// mengirimkan komentar untuk{" "}
// <span className="font-bold text-sm">
// {ticketInternal?.feedTitle}
// </span>
// </p>
// <p className="text-xs">
// {`${new Date(ticketInternal?.createdAt).getDate()}-${
// new Date(ticketInternal?.createdAt).getMonth() + 1
// }-${new Date(
// ticketInternal?.createdAt
// ).getFullYear()} ${new Date(
// ticketInternal?.createdAt
// ).getHours()}:${new Date(
// ticketInternal?.createdAt
// ).getMinutes()}`}
// </p>
// </div>
// </div>
// <div className="p-4 bg-white text-sm">
// <p>{htmlToString(ticketInternal.message)}</p>
// {detailTickets?.typeId === 6 &&
// detailTickets?.emergencyIssue ? (
// <div className="row mx-0 mb-3 emergency-attachments">
// <div className="mt-3 mr-4">
// <Button
// color="primary"
// size="md"
// onClick={openEmergencyIssueDetail}
// >
// Info Lainnya
// </Button>
// </div>
// </div>
// ) : null}
// {detailTickets?.emergencyIssue && (
// <InfoLainnyaModal
// open={openEmergencyModal}
// onClose={() => setOpenEmergencyModal(false)}
// data={detailTickets.emergencyIssue}
// />
// )}
// </div>
// </div>
// )}
// </div>
// </div>
// {detail !== undefined && (
// <div className="gap-5 mb-5 w-[100%] lg:w-[30%] border bg-white rounded-md">
// <p className="mx-3 mt-3">Properties</p>
// <div className="space-y-2 px-3">
// <Label>Judul</Label>
// <Controller
// control={control}
// name="title"
// render={({ field }) => (
// <Input
// size="md"
// type="text"
// value={detail?.title}
// onChange={field.onChange}
// placeholder="Enter Title"
// />
// )}
// />
// {/* {errors.title?.message && (
// <p className="text-red-400 text-sm">
// {errors.title.message}
// </p>
// )} */}
// </div>
// <div className="mt-5 px-3">
// <Label>Prioritas</Label>
// <Select
// onValueChange={setSelectedPriority}
// value={detail?.priority?.name}
// >
// <SelectTrigger size="md">
// <SelectValue placeholder="Pilih" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="Low">Low</SelectItem>
// <SelectItem value="Medium">Medium</SelectItem>
// <SelectItem value="High">High</SelectItem>
// </SelectContent>
// </Select>
// </div>
// <div className="mt-5 px-3 mb-3">
// <Label>Status</Label>
// <Select
// onValueChange={setSelectedStatus}
// value={detail?.status?.name}
// >
// <SelectTrigger size="md">
// <SelectValue placeholder="Pilih" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="Open">Open</SelectItem>
// <SelectItem value="Close">Close</SelectItem>
// </SelectContent>
// </Select>
// </div>
// <div className="mt-5 px-3 mb-3">
// <Label>Operator</Label>
// {/* Tag yang ditampilkan secara kolom */}
// {selectedOperator.length > 0 && (
// <div className="flex flex-col gap-2 mb-2">
// {selectedOperator.map((id) => {
// const label = operatorOpt.find(
// (op: any) => op.value === id
// )?.label;
// return (
// <div
// key={id}
// className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
// >
// <span>{label}</span>
// <button
// type="button"
// onClick={() =>
// setSelectedOperator((prev) =>
// prev.filter((val) => val !== id)
// )
// }
// className="ml-2 text-gray-500 hover:text-red-600"
// >
// ×
// </button>
// </div>
// );
// })}
// </div>
// )}
// {/* Popover Checkbox Dropdown */}
// <Popover>
// <PopoverTrigger asChild>
// <Button variant="outline" className="w-full justify-between">
// Pilih Operator
// <ChevronDownIcon className="ml-2 h-4 w-4" />
// </Button>
// </PopoverTrigger>
// <PopoverContent className="w-full max-h-60 overflow-auto">
// <div className="flex flex-col gap-1">
// {operatorOpt.map((op: any) => (
// <label
// key={op.id}
// className="flex items-center space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
// >
// <input
// type="checkbox"
// checked={selectedOperator.includes(op.value)}
// disabled
// onChange={(e) => {
// if (e.target.checked) {
// setSelectedOperator((prev) => [
// ...prev,
// op.value,
// ]);
// } else {
// setSelectedOperator((prev) =>
// prev.filter((val) => val !== op.value)
// );
// }
// }}
// />
// <span>{op.label}</span>
// </label>
// ))}
// </div>
// </PopoverContent>
// </Popover>
// </div>
// <div className="space-y-2 px-3 py-3">
// <Label>Description</Label>
// <Controller
// control={control}
// name="title"
// render={({ field }) => (
// <Textarea
// value={detail?.description}
// onChange={field.onChange}
// placeholder="Enter Title"
// />
// )}
// />
// {/* {errors.title?.message && (
// <p className="text-red-400 text-sm">
// {errors.title.message}
// </p>
// )} */}
// </div>
// </div>
// )}
// </div>
// </div>
// );
// }
"use client";
import React, { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getTicketingDetail, getTicketingReply, saveTicketReply } from "@/service/service/communication/communication";
export type replyDetail = {
id: number;
comments: string;
createdAt: string;
user: { id: number; fullname: string };
messageTo: { id: number; fullname: string };
};
type Props = { id: string };
export default function FormDetailTicketing({ id }: Props) {
const MySwal = withReactContent(Swal);
const [detail, setDetail] = useState<any>(null);
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [replyText, setReplyText] = useState("");
const chatEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
async function init() {
const resDetail = await getTicketingDetail(id);
setDetail(resDetail?.data?.data);
await fetchReplies();
}
init();
}, [id]);
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [ticketReply]);
async function fetchReplies() {
const res = await getTicketingReply(id);
if (res?.data !== null) {
const sortedReplies = res?.data.data.sort(
(a: replyDetail, b: replyDetail) =>
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
setTicketReply(sortedReplies);
}
}
const sendReply = async (resolve: boolean) => {
if (!replyText.trim()) return;
try {
await saveTicketReply({
ticketId: id,
comment: replyText,
resolve,
});
setReplyText("");
await fetchReplies();
} catch (err) {
MySwal.fire("Error", "Gagal kirim balasan", "error");
}
};
const handleTranslate = () => {
MySwal.fire("Info", "Fitur translate belum dihubungkan", "info");
};
return (
<section className="flex flex-col h-full bg-white">
{/* Header */}
<div className="border-b px-4 py-3 shrink-0">
<div className="flex items-center gap-3">
<div className="text-sm font-medium">
{(detail?.title ?? "").split(" ").slice(0, 25).join(" ") +
((detail?.title ?? "").split(" ").length > 25 ? "..." : "")}
</div>
<div className="text-xs text-muted-foreground">
{detail?.source ?? "Ticket"}
</div>
</div>
</div>
{/* Chat Messages (scrollable only this part) */}
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
{!detail ? (
<div className="h-full flex items-center justify-center text-muted-foreground">
<div className="text-center">
<div className="mb-4">
{/* Icon kosong */}
<svg
width="72"
height="72"
viewBox="0 0 24 24"
className="mx-auto opacity-60"
>
<path
fill="currentColor"
d="M12 3C7 3 3 6.6 3 11c0 1.9.8 3.6 2.2 5v3.1L8 17.9c1 .3 2 .5 4 .5 5 0 9-3.6 9-8.1S17 3 12 3z"
/>
</svg>
</div>
<div className="text-sm">Pilih issue untuk melihat detail</div>
</div>
</div>
) : (
ticketReply.map((m) => {
const isUser = m.user.fullname !== "Agent";
return (
<div
key={m.id}
className={cn(
"flex items-end",
isUser ? "justify-end" : "justify-start"
)}
>
<div
className={cn(
"px-4 py-2 rounded-2xl max-w-[70%] break-words",
isUser
? "bg-green-500 text-white"
: "bg-white text-gray-800",
isUser ? "rounded-br-none" : "rounded-bl-none",
"shadow-sm"
)}
>
<div className="whitespace-pre-wrap text-sm">
{m.comments}
</div>
<div className="text-[10px] text-gray-300 mt-1 text-right">
{new Date(m.createdAt).toLocaleTimeString("id-ID", {
hour: "2-digit",
minute: "2-digit",
})}
</div>
</div>
</div>
);
})
)}
<div ref={chatEndRef} />
</div>
{/* Input Box */}
<div className="border-t px-4 py-3 bg-white shrink-0">
<div className="flex flex-col gap-2 max-w-full mx-auto">
<textarea
placeholder='Enter your reply or type "/" to insert a quick reply'
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
className="w-full border rounded-xl p-3 min-h-[64px] resize-none focus:outline-none focus:ring"
/>
<div className="flex items-center justify-between gap-3">
<Button variant="outline" size="sm" onClick={handleTranslate}>
Translate
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => sendReply(true)}
disabled={!replyText.trim()}
>
Kirim & Resolve
</Button>
<Button
size="sm"
onClick={() => sendReply(false)}
disabled={!replyText.trim()}
>
Kirim
</Button>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,581 @@
"use client";
"use client";
import React, { useEffect, useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import {
deleteTicket,
getTicketingDetail,
getTicketingInternalDetail,
getTicketingInternalDiscussion,
getTicketingReply,
saveTicketing,
saveTicketInternalReply,
saveTicketReply,
} from "@/service/communication/communication";
import { Icon } from "@iconify/react/dist/iconify.js";
import { list } from "postcss";
import { htmlToString } from "@/utils/globals";
import { Textarea } from "@/components/ui/textarea";
import { error } from "@/lib/swal";
import { useRouter } from "next/navigation";
import { getOperatorUser } from "@/service/management-user/management-user";
import { DetailTicket } from "./info-lainnya-types";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ChevronDownIcon } from "lucide-react";
import { Description } from "@radix-ui/react-toast";
const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z.string().min(2, {
message: "Narasi Penugasan harus lebih dari 2 karakter.",
}),
});
export type taskDetail = {
id: number;
title: string;
createdAt: string;
referenceNumber: string | number;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
status: {
id: number;
name: string;
};
priority: {
id: number;
name: string;
};
broadcastType: string;
description: string;
is_active: string;
};
export type replyDetail = {
id: number;
comments: string;
createdAt: string;
user: {
id: number;
fullname: string;
};
messageTo: {
id: number;
fullname: string;
};
};
export type internalDetail = {
id: number;
message: string;
createdAt: string;
createdBy: {
id: number;
fullname: string;
};
sendTo: {
id: number;
fullname: string;
};
};
export default function FormUpdateTicketing() {
const MySwal = withReactContent(Swal);
const { id } = useParams() as { id: string };
const router = useRouter();
const [detail, setDetail] = useState<taskDetail>();
const [ticketReply, setTicketReply] = useState<replyDetail[]>([]);
const [ticketInternal, setTicketInternal] = useState<internalDetail | null>(
null
);
const [detailTickets, setDetailTickets] = useState<DetailTicket | null>(null);
const [replyValue, setReplyValue] = useState<number>(0);
const [replyText, setReplyText] = useState<string>("");
const [operatorOpt, setOperatorOpt] = useState<
{ id: string; label: string; value: string }[]
>([]);
const [selectedOperator, setSelectedOperator] = useState<string[]>([]);
const [replyVisible, setReplyVisible] = useState(false);
const [replyMessage, setReplyMessage] = useState("");
const [selectedPriority, setSelectedPriority] = useState("");
const [selectedStatus, setSelectedStatus] = useState("");
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm({
resolver: zodResolver(taskSchema),
defaultValues: {
title: "",
description: "",
priority: "",
status: "",
},
});
useEffect(() => {
async function initState() {
setReplyValue(0);
const response = await getTicketingDetail(id);
setTicketInternal(response?.data?.data || null);
setDetail(response?.data?.data);
if (response?.data?.data) {
const detailData = response.data.data;
setValue("title", detailData.title || "");
setValue("description", detailData.description || "");
setSelectedPriority(detailData.priority?.name || "");
setSelectedStatus(detailData.status?.name || "");
}
if (response?.data !== null) {
setDetailTickets(response?.data?.data);
}
}
initState();
getTicketReply();
}, [id]);
useEffect(() => {
async function getOperator() {
const res = await getOperatorUser(detailTickets?.typeId);
if (res?.data !== null) {
const rawUser = res?.data?.data;
const optionArr = rawUser?.map((option: any) => ({
id: option.id,
label: option.fullName,
value: option.id.toString(),
}));
setOperatorOpt(optionArr);
if (detailTickets?.assignedTeams) {
const assigned = detailTickets.assignedTeams
.split(":")
.filter((id: string) => id);
setSelectedOperator(assigned);
}
}
}
getOperator();
}, [detailTickets]);
async function getTicketReply() {
const res = await getTicketingReply(id);
if (res?.data !== null) {
setTicketReply(res?.data?.data);
}
}
const handleReply = () => {
setReplyVisible((prev) => !prev);
};
const handleSendReply = async () => {
if (replyMessage.trim() === "") {
MySwal.fire({
title: "Error",
text: "Pesan tidak boleh kosong!",
icon: "error",
});
return;
}
const data = {
ticketId: id,
message: replyMessage,
};
try {
const response = await saveTicketReply(data);
const newReply: replyDetail = {
id: response?.data?.id,
comments: replyMessage,
createdAt: response?.data?.createdAt,
user: response?.data?.messageFrom,
messageTo: response?.data?.messageTo,
};
setTicketReply((prevReplies) => [newReply, ...prevReplies]);
MySwal.fire({
title: "Sukses",
text: "Pesan berhasil dikirim.",
icon: "success",
});
setReplyMessage("");
setReplyVisible(false);
} catch (error) {
MySwal.fire({
title: "Error",
text: "Gagal mengirim balasan.",
icon: "error",
});
console.error("Error sending reply:", error);
}
};
async function doDelete(id: any) {
const response = await deleteTicket(id);
if (response?.error) {
error(response.message);
return false;
}
success("/in/supervisor/ticketing");
}
function success(redirect: string) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
}
const handleDelete = (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);
}
});
};
async function save(data: any) {
const reqData: any = {
title: data.title,
description: data.description,
priority: data.priority,
status: data.status,
operatorTeam: selectedOperator.join(","),
};
if (id) {
reqData.id = id;
}
const response = await saveTicketing(reqData);
if (response?.error) {
MySwal.fire({
title: "Error",
text: response.message,
icon: "error",
});
return false;
}
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonText: "OK",
}).then(() => {
router.push(`/in/supervisor/ticketing`);
});
}
return (
<div className="py-5">
<div className="mt-4 flex flex-row items-center gap-3">
<Button onClick={handleReply} color="default" variant={"outline"}>
Balas
</Button>
{/* <Button onClick={handleDelete} color="default" variant={"outline"}>
Hapus
</Button> */}
</div>
<div className="flex flex-col md:flex-row lg:flex-row gap-5 mt-5">
<div className="flex flex-col w-[100%] lg:w-[70%]">
{replyVisible && (
<div className="">
<textarea
id="replyMessage"
className="w-full h-24 border rounded-md p-2"
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
placeholder="Tulis pesan di sini..."
/>
<div className="flex justify-end gap-3 my-2">
<Button
onClick={() => setReplyVisible(false)}
color="default"
variant="outline"
>
Batal
</Button>
<Button onClick={handleSendReply} color="primary">
Kirim
</Button>
</div>
</div>
)}
<div className="border rounded-t-xl">
<p className="p-4 bg-slate-300 rounded-t-xl text-lg font-semibold">
Ticket #{detail?.referenceNumber}
</p>
{ticketReply?.map((list) => (
<div key={list.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold text-sm">
{list?.user?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{list?.messageTo?.fullname}
</span>
</p>
<p className="text-xs">
{`${new Date(list?.createdAt).getDate()}-${
new Date(list?.createdAt).getMonth() + 1
}-${new Date(list?.createdAt).getFullYear()} ${new Date(
list?.createdAt
).getHours()}:${new Date(list?.createdAt).getMinutes()}`}
</p>
</div>
</div>
<p className="p-4 bg-white text-sm">{list.comments}</p>
</div>
))}
{ticketInternal && (
<div key={ticketInternal.id} className="flex flex-col">
<div className="flex flex-row gap-3 bg-sky-100 p-4 items-center">
<Icon icon="qlementine-icons:user-16" width={36} />
<div>
<p>
<span className="font-bold text-sm">
{ticketInternal?.createdBy?.fullname}
</span>{" "}
mengirimkan pesan untuk{" "}
<span className="font-bold text-sm">
{ticketInternal?.sendTo?.fullname}
</span>
</p>
<p className="text-xs">
{`${new Date(ticketInternal?.createdAt).getDate()}-${
new Date(ticketInternal?.createdAt).getMonth() + 1
}-${new Date(
ticketInternal?.createdAt
).getFullYear()} ${new Date(
ticketInternal?.createdAt
).getHours()}:${new Date(
ticketInternal?.createdAt
).getMinutes()}`}
</p>
</div>
</div>
<p className="p-4 bg-white text-sm">
{htmlToString(ticketInternal.message)}
</p>
</div>
)}
</div>
</div>
{detail !== undefined && (
<div className="gap-5 mb-5 w-[100%] lg:w-[30%] border bg-white rounded-md">
<form onSubmit={handleSubmit(save)}>
<p className="mx-3 mt-3">Properties</p>
<div className="space-y-2 px-3">
<Label>Judul</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input
{...field}
size="md"
type="text"
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="mt-5 px-3">
<Label>Prioritas</Label>
<Select
onValueChange={setSelectedPriority}
value={detail?.priority?.name}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Low">Low</SelectItem>
<SelectItem value="Medium">Medium</SelectItem>
<SelectItem value="High">High</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Status</Label>
<Select
onValueChange={setSelectedStatus}
value={detail?.status?.name}
>
<SelectTrigger size="md">
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Open">Open</SelectItem>
<SelectItem value="Close">Close</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-5 px-3 mb-3">
<Label>Operator</Label>
{/* Tag yang ditampilkan secara kolom */}
{selectedOperator.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{selectedOperator.map((id) => {
const label = operatorOpt.find(
(op: any) => op.value === id
)?.label;
return (
<div
key={id}
className="flex items-center justify-between bg-gray-200 px-3 py-1 rounded"
>
<span>{label}</span>
<button
type="button"
onClick={() =>
setSelectedOperator((prev) =>
prev.filter((val) => val !== id)
)
}
className="ml-2 text-gray-500 hover:text-red-600"
>
×
</button>
</div>
);
})}
</div>
)}
{/* Popover Checkbox Dropdown */}
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="w-full justify-between"
>
Pilih Operator
<ChevronDownIcon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full max-h-60 overflow-auto">
<div className="flex flex-col gap-1">
{operatorOpt.map((op: any) => (
<label
key={op.id}
className="flex items-center space-x-2 cursor-pointer px-2 py-1 hover:bg-gray-100 rounded"
>
<input
type="checkbox"
checked={selectedOperator.includes(op.value)}
onChange={(e) => {
if (e.target.checked) {
setSelectedOperator((prev) => [
...prev,
op.value,
]);
} else {
setSelectedOperator((prev) =>
prev.filter((val) => val !== op.value)
);
}
}}
/>
<span>{op.label}</span>
</label>
))}
</div>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2 px-3 py-3">
<Label>Description</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<Textarea
defaultValue={detail?.description}
onChange={field.onChange}
placeholder="Enter Title"
/>
)}
/>
{/* {errors.title?.message && (
<p className="text-red-400 text-sm">
{errors.title.message}
</p>
)} */}
</div>
<div className="flex justify-end mt-3 mr-3 py-3">
<Button type="submit" color="primary">
Update
</Button>
</div>
</form>
</div>
)}
</div>
</div>
);
}

View File

@ -94,7 +94,7 @@ export default function Navbar() {
<div className="flex flex-row items-center justify-between space-x-4 z-10">
<div className="relative w-32 h-20">
<Image
src="/netidhub.png"
src="/assets/logo1.png"
alt="Logo"
fill
className="object-contain"
@ -127,7 +127,7 @@ export default function Navbar() {
{item.label}
<span
className={cn(
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-yellow-600 rounded transition-all",
"absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded transition-all",
isDropdownOpen ||
pathname.startsWith("/public/publication")
? "opacity-100"
@ -160,7 +160,7 @@ export default function Navbar() {
>
{item.label}
{isActive && (
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-yellow-600 rounded" />
<span className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-6 h-[3px] bg-red-800 rounded" />
)}
</Link>
)}
@ -172,12 +172,12 @@ export default function Navbar() {
{!isLoggedIn ? (
<>
<Link href="/auth/register">
<Button className="bg-transparent border text-black hover:bg-[#C6A455] hover:text-white">
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white">
Daftar
</Button>
</Link>
<Link href="/auth">
<Button className="bg-[#C6A455] text-white">Masuk</Button>
<Button className="bg-red-700 text-white">Masuk</Button>
</Link>
</>
) : (

View File

@ -31,8 +31,8 @@ const Logo = () => {
<h1 className="text-xl font-semibold text-default-900 ">D</h1>
)} */}
<img
className="w-[100px]"
src="/logo-netidhub.png"
className="w-[180px]"
src="/assets/logo1.png"
alt="logo"
/>
</Link>

Some files were not shown because too many files have changed in this diff Show More