This commit is contained in:
hanif salafi 2025-06-23 23:22:51 +07:00
commit 199aaf5808
9 changed files with 677 additions and 266 deletions

View File

@ -9,12 +9,7 @@ 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 { Book, CheckCheck, Plus, Timer } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { EventContentArg } from "@fullcalendar/core";
import EventModal from "./event-modal";
@ -124,7 +119,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const [calendarEvents, setCalendarEvents] = useState<CalendarEvent[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const t = useTranslations("CalendarApp");
// Modal states
const [sheetOpen, setSheetOpen] = useState<boolean>(false);
const [date, setDate] = useState<Date>(new Date());
@ -171,7 +166,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const monthData = res?.data?.data;
if (monthData) {
const allEvents: CalendarEvent[] = [];
// Map API data to the calendarEvents structure
// Map API data to the calendarEvents structure
const events = monthData?.map((event: any) => ({
id: event.id.toString(),
title: event.title,
@ -207,7 +202,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
"",
""
);
if (res?.error) {
error(res?.message || "Failed to fetch yearly events");
return;
@ -230,17 +225,20 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
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"];
// Cek apakah SEMUA validTypeIds ada di typeIdsInData
const hasAllCategories = allCategoryId.every(categoryId => selectedCategory.includes(categoryId));
const hasAllCategories = allCategoryId.every((categoryId) =>
selectedCategory.includes(categoryId)
);
return eventCategories?.some((cat: string) =>
selectedCategory.includes(cat) || (hasAllCategories && cat == "0")
return eventCategories?.some(
(cat: string) =>
selectedCategory.includes(cat) || (hasAllCategories && cat == "0")
);
});
@ -266,7 +264,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
};
const handleCategorySelection = (category: string) => {
setSelectedCategory(prev => {
setSelectedCategory((prev) => {
if (prev.includes(category)) {
return prev.filter((c) => c !== category);
} else {
@ -313,7 +311,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const getEventColor = (type: EventType): string => {
const typeSplit = type.split(",");
const firstType = typeSplit[0] as EventType;
const colors: Record<EventType, string> = {
"0": "bg-black",
"1": "bg-yellow-500",
@ -322,7 +320,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
"4": "bg-orange-500",
"5": "bg-green-400",
};
return colors[firstType] || "bg-gray-400";
};
@ -346,15 +344,22 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
const renderEventContent = (eventInfo: any) => {
const { title } = eventInfo.event;
const { createdByName, isPublish, calendar } = eventInfo.event.extendedProps;
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={`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} />}
{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">
@ -386,7 +391,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
bgColor,
colorList,
}) => (
<div
<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)}
>
@ -461,8 +466,12 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
text={event.title}
createdBy={event.createdByName}
isPublish={event.isPublish}
bgColor={getEventColor(event.agendaType as EventType)}
colorList={getEventColorList(event.agendaType as EventType)}
bgColor={getEventColor(
event.agendaType as EventType
)}
colorList={getEventColorList(
event.agendaType as EventType
)}
/>
))}
</div>
@ -537,7 +546,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
{t("addEvent")}
</Button>
)}
{roleId === 3 && userLevelId === 216 && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
@ -593,7 +602,9 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
className={category.className}
id={category.label}
checked={selectedCategory.includes(category.value)}
onCheckedChange={() => handleCategorySelection(category.value)}
onCheckedChange={() =>
handleCategorySelection(category.value)
}
/>
<Label htmlFor={category.label}>{category.label}</Label>
</li>
@ -605,7 +616,12 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
<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]}
plugins={[
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
listPlugin,
]}
headerToolbar={{
left: "prev,next today",
center: "title",
@ -636,7 +652,9 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
handleDateChange(info.view.currentStart, info.view.currentEnd);
handleViewChange(info.view.type);
}}
viewClassNames={activeView === "listYear" ? "hide-calendar-grid" : ""}
viewClassNames={
activeView === "listYear" ? "hide-calendar-grid" : ""
}
/>
{activeView === "listYear" && (
@ -657,7 +675,7 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
</CardContent>
</Card>
</div>
<EventModal
open={sheetOpen}
onClose={handleCloseModal}
@ -669,4 +687,4 @@ const CalendarView = ({ categories }: CalendarViewProps) => {
);
};
export default CalendarView;
export default CalendarView;

View File

@ -148,10 +148,12 @@ const EventModal = ({
satker: false,
international: false,
});
const [agendaType, setAgendaType] = React.useState(""); // State untuk agendaType
const [selectedPolda, setSelectedPolda] = React.useState([]); // Untuk data Polda
const [selectedSatker, setSelectedSatker] = React.useState([]);
const [selectedPolres, setSelectedPolres] = React.useState([]);
const [selectedPolda, setSelectedPolda] = useState<string[]>([]);
const [selectedPolres, setSelectedPolres] = useState<string[]>([]);
const [selectedSatker, setSelectedSatker] = useState<string[]>([]);
const isDetailMode = true;
const [wavesurfer, setWavesurfer] = useState<WaveSurfer>();
const [isPlaying, setIsPlaying] = useState(false);
const [isPublishing, setIsPublishing] = useState(false);
@ -174,10 +176,7 @@ const EventModal = ({
const detail = res?.data?.data;
setDetailData(detail);
const description = res?.data?.data?.description;
console.log("description", res?.data?.data?.description);
// Set nilai awal description ke form control
const description = detail?.description;
if (description) {
setValue("description", description);
}
@ -196,17 +195,56 @@ const EventModal = ({
attachments?.filter((file: any) => file.fileTypeId == 4)
);
const agendaType = detail?.agendaType;
setWilayahPublish({
semua: agendaType === "all",
nasional: agendaType === "mabes",
polda: agendaType === "polda",
polres: agendaType === "polres",
satker: agendaType === "satker",
international: agendaType === "international",
});
}
const rawAgendaTypes = detail?.agendaType?.split(",") || []; // ["0","1","2","3","4","5"]
const assignedToLevel = detail?.assignedToLevel?.split(",") || [];
const wilayahState = {
semua: false,
nasional: false,
polda: false,
polres: false,
satker: false,
international: false,
};
rawAgendaTypes.forEach((type: any) => {
switch (type) {
case "0":
wilayahState.semua = true;
break;
case "1":
wilayahState.nasional = true;
break;
case "2":
wilayahState.polda = true;
break;
case "3":
wilayahState.polres = true;
break;
case "4":
wilayahState.satker = true;
break;
case "5":
wilayahState.international = true;
break;
default:
break;
}
});
setWilayahPublish(wilayahState);
// Atur unit berdasarkan agendaType
if (rawAgendaTypes.includes("2")) {
setSelectedPolda(assignedToLevel);
}
if (rawAgendaTypes.includes("3")) {
setSelectedPolres(assignedToLevel);
}
if (rawAgendaTypes.includes("4")) {
setSelectedSatker(assignedToLevel);
}
}
fetchDetailData();
}, [event, setValue]);
@ -233,33 +271,39 @@ const EventModal = ({
const toggleWilayah = (key: string) => {
setWilayahPublish((prev: any) => {
const newState = { ...prev, [key]: !prev[key] };
let newState = { ...prev };
// Handle "semua" logic to check all options
if (key === "semua" && newState.semua) {
setAgendaType("all");
return {
semua: true,
nasional: true,
polda: true,
polres: true,
satker: true,
international: true,
// Jika key === semua dan sebelumnya belum aktif, aktifkan semua
if (key === "semua") {
const newChecked = !prev.semua;
newState = {
semua: newChecked,
nasional: newChecked,
polda: newChecked,
polres: newChecked,
satker: newChecked,
international: newChecked,
};
if (newChecked) {
setAgendaType("0,1,2,3,4,5");
} else {
setAgendaType("");
}
return newState;
}
// Uncheck "semua" if any other option is selected
if (key !== "semua") {
newState.semua = false;
}
// Jika key bukan "semua"
newState[key] = !prev[key];
newState.semua = false; // Uncheck "semua" jika yang dipilih adalah individu
// Set agendaType based on the selected checkbox
if (newState.nasional) setAgendaType("mabes");
else if (newState.polda) setAgendaType("polda");
else if (newState.polres) setAgendaType("polres");
else if (newState.satker) setAgendaType("satker");
else if (newState.international) setAgendaType("international");
else setAgendaType(""); // Reset if no checkbox is selected
// Hitung ulang agendaType berdasarkan pilihan
const selectedKeys = Object.entries(newState)
.filter(([k, v]) => v && k !== "semua")
.map(([k]) => wilayahValueMap[k]);
setAgendaType(selectedKeys.join(","));
return newState;
});
@ -269,12 +313,25 @@ const EventModal = ({
const agendaTypeList: string[] = [];
const assignedToLevelList: string[] = [];
// Mapping dari checkbox wilayah ke agendaType
Object.keys(wilayahPublish).forEach((key) => {
if (wilayahPublish[key as keyof typeof wilayahPublish]) {
agendaTypeList.push(wilayahValueMap[key]);
}
});
// // Mapping dari checkbox wilayah ke agendaType
// Object.keys(wilayahPublish).forEach((key) => {
// if (wilayahPublish[key as keyof typeof wilayahPublish]) {
// agendaTypeList.push(wilayahValueMap[key]);
// }
// });
if (wilayahPublish.semua) {
agendaTypeList.push("0", "1", "2", "3", "4", "5");
} else {
Object.keys(wilayahPublish).forEach((key) => {
if (
wilayahPublish[key as keyof typeof wilayahPublish] &&
key !== "semua"
) {
agendaTypeList.push(wilayahValueMap[key]);
}
});
}
// Unit-unit berdasarkan wilayah yang aktif
if (wilayahPublish.polda && selectedPolda.length > 0) {
@ -558,6 +615,7 @@ const EventModal = ({
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
window.location.reload();
});
};
@ -784,7 +842,8 @@ const EventModal = ({
{wilayahPublish.polda && (
<UnitMapping
unit="Polda"
isDetail={false}
isDetail={isDetailMode} // jika Anda punya kondisi detail
initData={selectedPolda}
sendDataToParent={(data: any) =>
setSelectedPolda(data)
}
@ -802,8 +861,9 @@ const EventModal = ({
</label>
{wilayahPublish.polres && (
<UnitMapping
isDetail={false}
unit="Polres"
isDetail={isDetailMode}
initData={selectedPolres}
sendDataToParent={(data: any) =>
setSelectedPolres(data)
}
@ -821,8 +881,9 @@ const EventModal = ({
</label>
{wilayahPublish.satker && (
<UnitMapping
isDetail={false}
unit="Satker"
isDetail={isDetailMode}
initData={selectedSatker}
sendDataToParent={(data: any) =>
setSelectedSatker(data)
}

View File

@ -202,6 +202,23 @@ export default function FormAskExpert() {
const details = response?.data?.data;
setDetail(details);
if (details?.assignedToLevel) {
const levels: Set<number> = new Set(
details.assignedToLevel.split(",").map((x: any) => Number(x))
);
setCheckedLevels(levels);
}
if (details?.assignedToUsers) {
const userIds = details.assignedToUsers.split(",").map(Number);
setCheckedLevels(new Set(userIds));
}
if (details?.expertCompetencies) {
const compIds = details.expertCompetencies.split(",").map(Number);
setSelectedCompetencies(new Set(compIds));
}
}
}
initState();
@ -515,7 +532,7 @@ export default function FormAskExpert() {
return (
<Card>
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-task")}</p>
<p className="text-lg font-semibold mb-3">{t("form-task-ta")}</p>
{detail !== undefined ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">
@ -617,7 +634,14 @@ export default function FormAskExpert() {
}
className="mr-3"
/>
{expert.fullname}
<div className="flex flex-col gap-2">
<div className="font-bold">
{expert.fullname}
</div>
<div className="italic">
({expert.username})
</div>
</div>
</Label>
</div>
))}
@ -625,6 +649,46 @@ export default function FormAskExpert() {
</DialogContent>
</Dialog>
</div>
{checkedLevels.size > 0 && (
<div className="mt-3">
<Label className="text-sm text-gray-600 mb-2 block">
Tenaga Ahli Terpilih ({checkedLevels.size})
</Label>
<div className="flex flex-wrap gap-2">
{Array.from(checkedLevels).map((expertId) => {
const expert = listExpert?.find(
(exp: any) => exp.id === expertId
);
return expert ? (
<div
key={expert.id}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full border border-blue-200"
>
<span>{expert.fullname}</span>
<button
type="button"
onClick={() => handleCheckboxChange(expert.id)}
className="ml-1 text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-0.5 transition-colors"
title="Remove expert"
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
) : null;
})}
</div>
</div>
)}
</div>
<div className="mt-5 space-y-2">

View File

@ -636,7 +636,7 @@ export default function FormDoItYourself() {
return (
<Card>
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">{t("form-task")}</p>
<p className="text-lg font-semibold mb-3">{t("form-task-ta-do")}</p>
{detail !== undefined ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="gap-5 mb-5">

View File

@ -10,42 +10,23 @@ 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 JoditEditor from "jodit-react";
import {
acceptAssignment,
acceptAssignmentTa,
createAssignmentResponse,
createTask,
deleteAssignmentResponse,
deleteTask,
finishTask,
finishTaskTa,
getAcceptance,
getAcceptanceAssignmentStatus,
getAssignmentResponseList,
getMediaUpload,
getMediaUploadTa,
getTask,
getTaskTa,
getUserLevelForAssignments,
getUserLevelForExpert,
} from "@/service/task";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
ChevronDown,
ChevronUp,
Dock,
DotSquare,
ImageIcon,
@ -61,14 +42,20 @@ import { close, error, loading } from "@/lib/swal";
import { getCookiesDecrypt } from "@/lib/utils";
import { Avatar, AvatarImage } from "@/components/ui/avatar";
import { successCallback } from "@/config/swal";
import FileUploader from "../shared/file-uploader";
import { AudioRecorder } from "react-audio-voice-recorder";
import Image from "next/image";
import { Icon } from "@iconify/react/dist/iconify.js";
import WavesurferPlayer from "@wavesurfer/react";
import WaveSurfer from "wavesurfer.js";
import { InputGroup, InputGroupText } from "@/components/ui/input-group";
import { useTranslations } from "next-intl";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { getListCompetencies } from "@/service/management-user/management-user";
const taskSchema = z.object({
uniqueCode: z.string().min(1, { message: "Judul diperlukan" }),
@ -252,7 +239,11 @@ export default function FormTaskTaDetail() {
const [acceptAcceptance, setAcceptAcceptance] = useState<AcceptanceData[]>(
[]
);
const [listExpert, setListExpert] = useState<any[]>([]);
const [userCompetencies, setUserCompetencies] = useState<any[]>([]);
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set()
);
const [totalPage, setTotalPage] = React.useState(1);
const [dataTable, setDataTable] = React.useState<any[]>([]);
const [totalData, setTotalData] = React.useState<number>(1);
@ -327,37 +318,37 @@ export default function FormTaskTaDetail() {
// setPlatformTypeVisible(selectedValue === 2);
// };
const handleExpertiseOutputChange = (
key: keyof typeof expertise,
value: boolean
) => {
if (key === "semua") {
const newState = {
semua: value,
komunikasi: value,
hukum: value,
bahasa: value,
ekonomi: value,
politik: value,
sosiologi: value,
ilmuadministrasipemerintah: value,
ti: value,
};
setExpertiseOutput(newState);
} else {
const updated = {
...expertise,
[key]: value,
};
// const handleExpertiseOutputChange = (
// key: keyof typeof expertise,
// value: boolean
// ) => {
// if (key === "semua") {
// const newState = {
// semua: value,
// komunikasi: value,
// hukum: value,
// bahasa: value,
// ekonomi: value,
// politik: value,
// sosiologi: value,
// ilmuadministrasipemerintah: value,
// ti: value,
// };
// setExpertiseOutput(newState);
// } else {
// const updated = {
// ...expertise,
// [key]: value,
// };
const allChecked = ["video", "audio", "image", "text"].every(
(k) => updated[k as keyof typeof expertise]
);
// const allChecked = ["video", "audio", "image", "text"].every(
// (k) => updated[k as keyof typeof expertise]
// );
updated.semua = allChecked;
setExpertiseOutput(updated);
}
};
// updated.semua = allChecked;
// setExpertiseOutput(updated);
// }
// };
const handleExpertOutputChange = (
key: keyof typeof expert,
@ -383,6 +374,68 @@ export default function FormTaskTaDetail() {
}
};
useEffect(() => {
getDataAdditional();
}, []);
async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
console.log("competency", resCompetencies);
setUserCompetencies(resCompetencies?.data?.data);
}
useEffect(() => {
async function fetchListExpert() {
setIsLoading(true);
try {
const response = await getUserLevelForExpert(id);
setListExpert(response?.data?.data);
console.log("tenaga ahli", response?.data?.data);
} catch (error) {
console.error("Error fetching Polda/Polres data:", error);
} finally {
setIsLoading(false);
}
}
fetchListExpert();
}, []);
useEffect(() => {
const fetchExpertsForCompetencies = async () => {
const allExperts: any[] = [];
for (const compId of Array.from(selectedCompetencies)) {
const response = await getUserLevelForExpert(compId);
const experts = response?.data?.data || [];
allExperts.push(...experts);
}
const uniqueExperts = Array.from(
new Map(allExperts.map((e) => [e.id, e])).values()
);
setListExpert(uniqueExperts);
};
if (selectedCompetencies.size > 0) {
fetchExpertsForCompetencies();
} else {
setListExpert([]);
}
}, [selectedCompetencies]);
const handleCompetencyChange = async (competencyId: number) => {
setSelectedCompetencies((prev) => {
const updated = new Set(prev);
if (updated.has(competencyId)) {
updated.delete(competencyId);
} else {
updated.add(competencyId);
}
return updated;
});
};
useEffect(() => {
async function fetchPoldaPolres() {
setIsLoading(true);
@ -458,6 +511,16 @@ export default function FormTaskTaDetail() {
setCheckedLevels(levels);
}
if (details?.assignedToUsers) {
const userIds = details.assignedToUsers.split(",").map(Number);
setCheckedLevels(new Set(userIds));
}
if (details?.expertCompetencies) {
const compIds = details.expertCompetencies.split(",").map(Number);
setSelectedCompetencies(new Set(compIds));
}
const attachment = details?.files;
setImageUploadedFiles(
attachment?.filter((file: any) => file.fileTypeId == 1)
@ -542,7 +605,7 @@ export default function FormTaskTaDetail() {
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/en/contributor/task");
router.push("/en/contributor/task-ta");
}
});
};
@ -682,7 +745,7 @@ export default function FormTaskTaDetail() {
const handleAcceptAcceptance = async () => {
loading();
console.log("Id user :", userId);
const response = await acceptAssignment(id);
const response = await acceptAssignmentTa(id);
if (response?.error) {
error(response?.message);
@ -773,7 +836,7 @@ export default function FormTaskTaDetail() {
);
async function finishAssignment() {
const response = finishTask(id);
const response = finishTaskTa(id);
// if (response.error) {
// error(response.message);
@ -1011,48 +1074,98 @@ export default function FormTaskTaDetail() {
<div className="mt-5 space-y-2">
<Label>{t("areas-expertise")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expertise).map((key) => (
<div className="flex items-center gap-2" key={key}>
{userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}>
<Checkbox
id={key}
checked={expertise[key as keyof typeof expertise]}
onCheckedChange={(value) =>
handleExpertiseOutputChange(
key as keyof typeof expertise,
value as boolean
)
}
id={`comp-${item.id}`}
checked={selectedCompetencies.has(item.id)}
onCheckedChange={() => handleCompetencyChange(item.id)}
disabled
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
</div>
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("choose-expert")}</Label>
{/* <Label>{t("choose-expert")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expert).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={expert[key as keyof typeof expert]}
onCheckedChange={(value) =>
handleExpertOutputChange(
key as keyof typeof expert,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{"Pilih Tenaga Ahli"}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>Daftar Tenaga Ahli</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listExpert?.map((expert: any) => (
<div key={expert.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(expert.id)}
onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
<div className="flex flex-col gap-2">
<div className="font-bold">
{expert.fullname}
</div>
<div className="italic">
({expert.username})
</div>
</div>
</Label>
</div>
))}
</div>
</DialogContent>
</Dialog>
</div> */}
{checkedLevels.size > 0 && (
<div className="mt-3">
<Label className="text-sm text-gray-600 mb-2 block">
Tenaga Ahli Terpilih ({checkedLevels.size})
</Label>
<div className="flex flex-wrap gap-2">
{Array.from(checkedLevels).map((expertId) => {
const expert = listExpert?.find(
(exp: any) => exp.id === expertId
);
return expert ? (
<div
key={expert.id}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full border border-blue-200"
>
<span>{expert.fullname}</span>
<button
type="button"
onClick={() => handleCheckboxChange(expert.id)}
className="ml-1 text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-0.5 transition-colors"
title="Remove expert"
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
) : null;
})}
</div>
))}
</div>
</div>
)}
</div>
<div></div>

View File

@ -26,6 +26,7 @@ import {
getTask,
getTaskTa,
getUserLevelForAssignments,
getUserLevelForExpert,
} from "@/service/task";
import {
Dialog,
@ -40,6 +41,7 @@ import {
Dock,
ImageIcon,
Music,
Trash2,
VideoIcon,
} from "lucide-react";
import FileUploader from "../shared/file-uploader";
@ -52,6 +54,7 @@ import { Upload } from "tus-js-client";
import { error, loading } from "@/lib/swal";
import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
import { getListCompetencies } from "@/service/management-user/management-user";
const taskSchema = z.object({
// uniqueCode: z.string().min(1, { message: "Judul diperlukan" }),
@ -148,11 +151,18 @@ export default function FormTaskTaEdit() {
const [broadcastType, setBroadcastType] = useState<string>(""); // untuk Tipe Penugasan
const [type, setType] = useState<string>("1");
const [selectedTarget, setSelectedTarget] = useState("3,4");
const [listExpert, setListExpert] = useState<any[]>([]);
const [userCompetencies, setUserCompetencies] = useState<any[]>([]);
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set()
);
const [detail, setDetail] = useState<taskDetail>();
const [urlInputs, setUrlInputs] = useState<Url[]>([]);
const [refresh] = useState(false);
const [listDest, setListDest] = useState([]); // Data Polda dan Polres
const [checkedLevels, setCheckedLevels] = useState(new Set());
const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set());
const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false);
const [audioFile, setAudioFile] = useState<File | null>(null);
@ -247,6 +257,68 @@ export default function FormTaskTaEdit() {
}
};
useEffect(() => {
getDataAdditional();
}, []);
async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
console.log("competency", resCompetencies);
setUserCompetencies(resCompetencies?.data?.data);
}
useEffect(() => {
async function fetchListExpert() {
setIsLoading(true);
try {
const response = await getUserLevelForExpert(id);
setListExpert(response?.data?.data);
console.log("tenaga ahli", response?.data?.data);
} catch (error) {
console.error("Error fetching Polda/Polres data:", error);
} finally {
setIsLoading(false);
}
}
fetchListExpert();
}, []);
useEffect(() => {
const fetchExpertsForCompetencies = async () => {
const allExperts: any[] = [];
for (const compId of Array.from(selectedCompetencies)) {
const response = await getUserLevelForExpert(compId);
const experts = response?.data?.data || [];
allExperts.push(...experts);
}
const uniqueExperts = Array.from(
new Map(allExperts.map((e) => [e.id, e])).values()
);
setListExpert(uniqueExperts);
};
if (selectedCompetencies.size > 0) {
fetchExpertsForCompetencies();
} else {
setListExpert([]);
}
}, [selectedCompetencies]);
const handleCompetencyChange = async (competencyId: number) => {
setSelectedCompetencies((prev) => {
const updated = new Set(prev);
if (updated.has(competencyId)) {
updated.delete(competencyId);
} else {
updated.add(competencyId);
}
return updated;
});
};
useEffect(() => {
async function fetchPoldaPolres() {
setIsLoading(true);
@ -285,12 +357,22 @@ export default function FormTaskTaEdit() {
}
if (details?.assignedToLevel) {
const levels = new Set(
details.assignedToLevel.split(",").map(Number)
const levels: Set<number> = new Set(
details.assignedToLevel.split(",").map((x: any) => Number(x))
);
setCheckedLevels(levels);
}
if (details?.assignedToUsers) {
const userIds = details.assignedToUsers.split(",").map(Number);
setCheckedLevels(new Set(userIds));
}
if (details?.expertCompetencies) {
const compIds = details.expertCompetencies.split(",").map(Number);
setSelectedCompetencies(new Set(compIds));
}
const attachment = details?.files;
setImageUploadedFiles(
attachment?.filter((file: any) => file.fileTypeId == 1)
@ -376,7 +458,7 @@ export default function FormTaskTaEdit() {
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push("/en/contributor/task");
router.push("/en/contributor/task-ta");
}
});
};
@ -393,9 +475,9 @@ export default function FormTaskTaEdit() {
});
};
const handlePoldaPolresChange = () => {
return Array.from(checkedLevels).join(","); // Mengonversi Set ke string
};
// const handlePoldaPolresChange = () => {
// return Array.from(checkedLevels).join(","); // Mengonversi Set ke string
// };
const handleUnitChange = (
key: keyof typeof unitSelection,
@ -454,6 +536,10 @@ export default function FormTaskTaEdit() {
}
};
const handleExpertChange = () => {
return Array.from(checkedLevels).join(",");
};
const save = async (data: TaskSchema) => {
const fileTypeMapping = {
all: "1",
@ -483,16 +569,11 @@ export default function FormTaskTaEdit() {
const requestData: {
id?: any;
title: string;
assignedToLevel: any;
assignedToUsers: any;
assignmentTypeId: string;
fileTypeOutput: string;
narration: string;
platformType: string | null;
assignmentMainTypeId: any;
assignmentType: string;
assignedToRole: string;
broadcastType: string;
expertCompetencies: string;
attachmentUrl: string[];
} = {
@ -500,17 +581,12 @@ export default function FormTaskTaEdit() {
// assignmentType,
// assignmentCategory,
id: detail?.id || null,
assignedToLevel: handlePoldaPolresChange(),
assignedToUsers: assignmentPurposeString,
assignedToUsers: handleExpertChange(),
assignedToRole: selectedTarget,
assignmentType: taskType,
broadcastType: broadcastType,
assignmentMainTypeId: mainType,
assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
narration: data.naration,
platformType: "",
expertCompetencies: "1,2,3",
expertCompetencies: Array.from(selectedCompetencies).join(","),
title: data.title,
attachmentUrl: links,
};
@ -808,47 +884,97 @@ export default function FormTaskTaEdit() {
<div className="mt-5 space-y-2">
<Label>{t("areas-expertise")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expertise).map((key) => (
<div className="flex items-center gap-2" key={key}>
{userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={item.id}>
<Checkbox
id={key}
checked={expertise[key as keyof typeof expertise]}
onCheckedChange={(value) =>
handleExpertiseOutputChange(
key as keyof typeof expertise,
value as boolean
)
}
id={`comp-${item.id}`}
checked={selectedCompetencies.has(item.id)}
onCheckedChange={() => handleCompetencyChange(item.id)}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
<Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
</div>
))}
</div>
</div>
<div className="mt-5 space-y-2">
<Label>{t("choose-expert")}</Label>
<div className="flex flex-wrap gap-4">
{Object.keys(expert).map((key) => (
<div className="flex items-center gap-2" key={key}>
<Checkbox
id={key}
checked={expert[key as keyof typeof expert]}
onCheckedChange={(value) =>
handleExpertOutputChange(
key as keyof typeof expert,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{"Pilih Tenaga Ahli"}]
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
<DialogHeader>
<DialogTitle>Daftar Tenaga Ahli</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
{listExpert?.map((expert: any) => (
<div key={expert.id} className="border p-2">
<Label className="flex items-center">
<Checkbox
checked={checkedLevels.has(expert.id)}
onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
<div className="flex flex-col gap-2">
<div className="font-bold">
{expert.fullname}
</div>
<div className="italic">
({expert.username})
</div>
</div>
</Label>
</div>
))}
</div>
</DialogContent>
</Dialog>
</div>
{checkedLevels.size > 0 && (
<div className="mt-3">
<Label className="text-sm text-gray-600 mb-2 block">
Tenaga Ahli Terpilih ({checkedLevels.size})
</Label>
<div className="flex flex-wrap gap-2">
{Array.from(checkedLevels).map((expertId) => {
const expert = listExpert?.find(
(exp: any) => exp.id === expertId
);
return expert ? (
<div
key={expert.id}
className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 text-sm font-medium px-3 py-1.5 rounded-full border border-blue-200"
>
<span>{expert.fullname}</span>
<button
type="button"
onClick={() => handleCheckboxChange(expert.id)}
className="ml-1 text-blue-600 hover:text-blue-800 hover:bg-blue-200 rounded-full p-0.5 transition-colors"
title="Remove expert"
>
<svg
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
) : null;
})}
</div>
</div>
)}
</div>
<div className="mt-5 space-y-2">
@ -1079,13 +1205,42 @@ export default function FormTaskTaEdit() {
/>
</div>
))}
<button
type="button"
className="mt-4 bg-green-500 text-white px-4 py-2 rounded"
onClick={handleAddLink}
>
{t("add-links")}
</button>
<div className="mt-4 space-y-2">
<Label className="">{t("news-links")}</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"
className="mt-2 bg-blue-500 text-white px-4 py-2 rounded"
onClick={handleAddRow}
size="sm"
>
{t("add-links")}
</Button>
</div>
</div>
</div>
</div>

View File

@ -784,6 +784,8 @@
"assignment-type": "Assignment Type",
"description-task": "Description Task",
"form-task": "Form Task",
"form-task-ta": "Form Ask the Expert",
"form-task-ta-do": "Form Do it yourself",
"assignment-selection": "Assignment Recipient",
"custom": "Costum",
"assigment-type": "Assigment Type",

View File

@ -571,47 +571,33 @@
"divisionNews": "Berita Satker",
"areaCoverage": "Liputan Wilayah & Satker",
"calendar": "KALENDER ACARA",
"january": "Januari",
"february": "February",
"march": "March",
"may": "May",
"june": "Juni",
"july": "Juli",
"august": "Agustus",
"october": "Oktober",
"december": "Desember",
"eventList": "Daftar Acara",
"noEvent": "Tidak ada acara yang tersedia",
"eventDetails": "Detail Acara",
"date": "Tanggal:",
"selectEvent": "Pilih acara untuk melihat detail",
"regionalPolice": "Polda Jajaran",
"policeDivision": "Satuan Kerja Polri",
"close": "Tutup",
"survey1": "SURVEI KEPUASAN PENGGUNA MEDIAHUB POLRI",
"survey2": "Kami menghargai pendapat Anda! Survei ini bertujuan untuk meningkatkan kualitas layanan MediaHub Polri. Mohon luangkan waktu beberapa menit untuk mengisi survei ini.",
"survey3": "SURVEY SEKARANG",
"survey4": "SURVEI KEPUASAN PENGGUNA MEDIAHUB POLRI",
"survey5": "Kami menghargai pendapat Anda! Survei ini bertujuan untuk meningkatkan kualitas layanan MediaHub Polri.",
"survey6": "1. Seberapa sering Anda mengakses MediaHub Polri?",
"survey7": "2. Bagaimana pengalaman Anda dalam mengakses website ini?",
"survey8": "3. Seberapa puas Anda dengan informasi yang tersedia di MediaHub Polri?",
"survey9": "4. Apakah Anda merasa website ini membantu dalam mendapatkan informasi terkait Polri?",
"survey10": "5. Apa saran atau masukan Anda?"
"january": "Januari",
"february": "February",
"march": "March",
"may": "May",
"june": "Juni",
"july": "Juli",
"august": "Agustus",
"october": "Oktober",
"december": "Desember",
"eventList": "Daftar Acara",
"noEvent": "Tidak ada acara yang tersedia",
"eventDetails": "Detail Acara",
"date": "Tanggal:",
"selectEvent": "Pilih acara untuk melihat detail",
"regionalPolice": "Polda Jajaran",
"policeDivision": "Satuan Kerja Polri",
"close": "Tutup",
"survey1": "SURVEI KEPUASAN PENGGUNA MEDIAHUB POLRI",
"survey2": "Kami menghargai pendapat Anda! Survei ini bertujuan untuk meningkatkan kualitas layanan MediaHub Polri. Mohon luangkan waktu beberapa menit untuk mengisi survei ini.",
"survey3": "SURVEY SEKARANG",
"survey4": "SURVEI KEPUASAN PENGGUNA MEDIAHUB POLRI",
"survey5": "Kami menghargai pendapat Anda! Survei ini bertujuan untuk meningkatkan kualitas layanan MediaHub Polri.",
"survey6": "1. Seberapa sering Anda mengakses MediaHub Polri?",
"survey7": "2. Bagaimana pengalaman Anda dalam mengakses website ini?",
"survey8": "3. Seberapa puas Anda dengan informasi yang tersedia di MediaHub Polri?",
"survey9": "4. Apakah Anda merasa website ini membantu dalam mendapatkan informasi terkait Polri?",
"survey10": "5. Apa saran atau masukan Anda?"
},
"FilterPage": {
"image": "Foto",
@ -798,6 +784,8 @@
"assignment-type": "Jenis Penugasan",
"description-task": "Narasi Penugasan",
"form-task": "Form Penugasan",
"form-task-ta": "Form Tanya Tenaga Ahli",
"form-task-ta-do": "Form Tenaga Ahli Kurasi",
"assignment-selection": "Penerima Tugas",
"custom": "Kostum",
"assigment-type": "Jenis Tugas",

View File

@ -108,6 +108,11 @@ export async function acceptAssignment(id: any) {
return httpPostInterceptor(url, id);
}
export async function acceptAssignmentTa(id: any) {
const url = `assignment-expert/acceptance?id=${id}`;
return httpPostInterceptor(url, id);
}
export async function postFinishAcceptance(id: any) {
const url = `assignment/finish-acceptance?id=${id}`;
return httpPostInterceptor(url, id);
@ -128,6 +133,11 @@ export async function finishTask(id: any) {
return httpPostInterceptor(url);
}
export async function finishTaskTa(id: any) {
const url = `assignment-expert/finish?id=${id}`;
return httpPostInterceptor(url);
}
export async function listTaskTa(
page: any,
title: string = "",