[QUDO-53,QUDO-54] feat:update form create penugasan Ta, update sidebar SPV,update table hasil upload media Ta

This commit is contained in:
Anang Yusman 2025-05-07 21:21:55 +08:00
parent 1cd3019c6b
commit bd527e102c
7 changed files with 172 additions and 239 deletions

View File

@ -32,6 +32,7 @@ import {
saveUserRolePlacements, saveUserRolePlacements,
} from "@/service/management-user/management-user"; } from "@/service/management-user/management-user";
import { loading } from "@/config/swal"; import { loading } from "@/config/swal";
import { Eye, EyeOff } from "lucide-react";
const FormSchema = z.object({ const FormSchema = z.object({
name: z.string({ name: z.string({
@ -79,6 +80,14 @@ export default function AddExpertForm() {
const [userCompetencies, setUserCompetencies] = useState<any>(); const [userCompetencies, setUserCompetencies] = useState<any>();
const [userExperiences, setUserExperiences] = useState<any>(); const [userExperiences, setUserExperiences] = useState<any>();
const [userLevels, setUserLevels] = useState<any>(); const [userLevels, setUserLevels] = useState<any>();
const [passwordType, setPasswordType] = useState("password");
const [showPassword, setShowPassword] = useState(false);
const togglePasswordType = () => {
setPasswordType((prevType) =>
prevType === "password" ? "text" : "password"
);
};
const roleSelection = [ const roleSelection = [
{ {
@ -115,7 +124,7 @@ export default function AddExpertForm() {
username: data.username, username: data.username,
email: data.email, email: data.email,
password: data.password, password: data.password,
adress: "", address: "",
roleId: "EXP-ID", roleId: "EXP-ID",
phoneNumber: data.phoneNumber, phoneNumber: data.phoneNumber,
userCompetencyId: data.skills, userCompetencyId: data.skills,
@ -308,12 +317,22 @@ export default function AddExpertForm() {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<Input <div className="relative">
type="password" <Input
value={field.value} value={field.value}
placeholder="Masukkan Password" type={showPassword ? "text" : "password"}
onChange={field.onChange} placeholder="Masukkan Password"
/> onChange={field.onChange}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-default-500 hover:text-default-700"
tabIndex={-1}
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
</div>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}

View File

@ -16,7 +16,7 @@ import { Link } from "@/components/navigation";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { deleteCategory } from "@/service/settings/settings"; import { deleteCategory } from "@/service/settings/settings";
import { deleteTask } from "@/service/task"; import { deleteTaskTa } from "@/service/task";
import { error, loading } from "@/lib/swal"; import { error, loading } from "@/lib/swal";
import withReactContent from "sweetalert2-react-content"; import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2"; import Swal from "sweetalert2";
@ -109,7 +109,7 @@ const useTableColumns = () => {
async function deleteProcess(id: any) { async function deleteProcess(id: any) {
loading(); loading();
const resDelete = await deleteTask(id); const resDelete = await deleteTaskTa(id);
if (resDelete?.error) { if (resDelete?.error) {
error(resDelete.message); error(resDelete.message);
@ -185,13 +185,15 @@ const useTableColumns = () => {
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
)} )}
<DropdownMenuItem {roleId == 11 && (
onClick={() => TaskDelete(row.original.id)} <DropdownMenuItem
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-destructive-foreground rounded-none" 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 <Trash2 className="w-4 h-4 me-1.5" />
</DropdownMenuItem> Delete
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@ -31,6 +31,7 @@ import {
getAcceptanceAssignmentStatus, getAcceptanceAssignmentStatus,
getAssignmentResponseList, getAssignmentResponseList,
getMediaUpload, getMediaUpload,
getMediaUploadTa,
getTask, getTask,
getTaskTa, getTaskTa,
getUserLevelForAssignments, getUserLevelForAssignments,
@ -407,7 +408,7 @@ export default function FormTaskTaDetail() {
const fetchAllData = async () => { const fetchAllData = async () => {
try { try {
const response = await getMediaUpload(id, userLevelId); const response = await getMediaUploadTa(id, userLevelId);
setUploadResults(response?.data?.data || []); setUploadResults(response?.data?.data || []);
} catch (error) { } catch (error) {
console.error("Error fetching all data:", error); console.error("Error fetching all data:", error);

View File

@ -25,6 +25,7 @@ import {
createTaskTa, createTaskTa,
getTask, getTask,
getUserLevelForAssignments, getUserLevelForAssignments,
getUserLevelForExpert,
} from "@/service/task"; } from "@/service/task";
import { import {
Dialog, Dialog,
@ -54,6 +55,11 @@ import { DateRange } from "react-day-picker";
import TimePicker from "react-time-picker"; import TimePicker from "react-time-picker";
import "react-time-picker/dist/TimePicker.css"; import "react-time-picker/dist/TimePicker.css";
import "react-clock/dist/Clock.css"; import "react-clock/dist/Clock.css";
import {
AdministrationLevelList,
getListCompetencies,
getListExperiences,
} from "@/service/management-user/management-user";
const taskSchema = z.object({ const taskSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "Judul diperlukan" }),
@ -129,7 +135,14 @@ export default function FormTaskTa() {
const [detail, setDetail] = useState<taskDetail>(); const [detail, setDetail] = useState<taskDetail>();
const [refresh] = useState(false); const [refresh] = useState(false);
const [listDest, setListDest] = useState([]); const [listDest, setListDest] = useState([]);
const [checkedLevels, setCheckedLevels] = useState(new Set()); const [userExperiences, setUserExperiences] = useState<any>();
const [userLevels, setUserLevels] = useState<any>();
const [userCompetencies, setUserCompetencies] = useState<any[]>([]);
const [selectedCompetencies, setSelectedCompetencies] = useState<Set<number>>(
new Set()
);
const [listExpert, setListExpert] = useState<any[]>([]);
const [checkedLevels, setCheckedLevels] = useState<Set<number>>(new Set());
const [expandedPolda, setExpandedPolda] = useState([{}]); const [expandedPolda, setExpandedPolda] = useState([{}]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [audioFile, setAudioFile] = useState<File | null>(null); const [audioFile, setAudioFile] = useState<File | null>(null);
@ -171,37 +184,57 @@ export default function FormTaskTa() {
mode: "all", mode: "all",
}); });
// const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => { useEffect(() => {
// const selectedValue = Number(event.target.value); getDataAdditional();
// setMainType(selectedValue); }, []);
// setPlatformTypeVisible(selectedValue === 2); async function getDataAdditional() {
const resCompetencies = await getListCompetencies();
console.log("competency", resCompetencies);
setUserCompetencies(resCompetencies?.data?.data);
}
useEffect(() => { useEffect(() => {
async function fetchPoldaPolres() { async function fetchListExpert() {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await getUserLevelForAssignments(); const response = await getUserLevelForExpert(id);
setListDest(response?.data?.data.list); setListExpert(response?.data?.data);
console.log("polda", response?.data?.data?.list); console.log("tenaga ahli", response?.data?.data);
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) { } catch (error) {
console.error("Error fetching Polda/Polres data:", error); console.error("Error fetching Polda/Polres data:", error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} }
fetchPoldaPolres(); 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);
}
// Hapus duplikat expert berdasarkan ID
const uniqueExperts = Array.from(
new Map(allExperts.map((e) => [e.id, e])).values()
);
setListExpert(uniqueExperts);
};
if (selectedCompetencies.size > 0) {
fetchExpertsForCompetencies();
} else {
setListExpert([]);
}
}, [selectedCompetencies]);
// }; // };
const handleCheckboxChange = (levelId: number) => { const handleCheckboxChange = (levelId: number) => {
setCheckedLevels((prev) => { setCheckedLevels((prev) => {
@ -215,10 +248,22 @@ export default function FormTaskTa() {
}); });
}; };
const handlePoldaPolresChange = () => { const handleExpertChange = () => {
return Array.from(checkedLevels).join(","); // Mengonversi Set ke string return Array.from(checkedLevels).join(","); // Mengonversi Set ke string
}; };
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;
});
};
const handleUnitChange = ( const handleUnitChange = (
key: keyof typeof unitSelection, key: keyof typeof unitSelection,
value: boolean value: boolean
@ -305,61 +350,22 @@ export default function FormTaskTa() {
}; };
const save = async (data: TaskSchema) => { const save = async (data: TaskSchema) => {
const fileTypeMapping = {
all: "1",
video: "2",
audio: "4",
image: "3",
text: "5",
};
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(expertise)
.filter((key) => expertise[key as keyof typeof expertise])
.map((key) => fileTypeMapping[key as keyof typeof fileTypeMapping])
.join(",");
const requestData: { const requestData: {
id?: number; id?: number;
title: string; title: string;
assignedToLevel: any;
assignedToUsers: any; assignedToUsers: any;
assignmentTypeId: string; assignmentTypeId: string;
fileTypeOutput: string;
narration: string; narration: string;
platformType: string | null;
assignmentMainTypeId: any;
assignmentType: string; assignmentType: string;
assignedToRole: string;
broadcastType: string;
expertCompetencies: string; expertCompetencies: string;
attachmentUrl: string[]; attachmentUrl: string[];
} = { } = {
...data, ...data,
// assignmentType, assignedToUsers: handleExpertChange(),
// assignmentCategory,
assignedToLevel: handlePoldaPolresChange(),
assignedToUsers: assignmentPurposeString,
assignedToRole: selectedTarget,
assignmentType: taskType, assignmentType: taskType,
broadcastType: broadcastType,
assignmentMainTypeId: mainType,
assignmentTypeId: type, assignmentTypeId: type,
fileTypeOutput: selectedOutputs,
narration: data.naration, narration: data.naration,
platformType: "", expertCompetencies: Array.from(selectedCompetencies).join(","),
expertCompetencies: "1,2,3",
title: data.title, title: data.title,
attachmentUrl: links, attachmentUrl: links,
}; };
@ -622,121 +628,7 @@ export default function FormTaskTa() {
<p className="text-red-400 text-sm">{errors.title.message}</p> <p className="text-red-400 text-sm">{errors.title.message}</p>
)} )}
</div> </div>
<div className="flex flex-col sm:flex-row lg:flex-row sm:items-center lg:items-center">
<div className="mt-5 space-y-2">
<Label>{t("assignment-selection")}</Label>
<Select onValueChange={setSelectedTarget}>
<SelectTrigger size="md">
<SelectValue placeholder="Choose" />
</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-5 lg:pt-7 lg:ml-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) =>
handleUnitChange(
key as keyof typeof unitSelection,
value as boolean
)
}
/>
<Label htmlFor={key}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div>
))}
</div>
<div className="mt-6 lg:pt-6 lg:pl-3">
<Dialog>
<DialogTrigger asChild>
<Button variant="soft" size="sm" color="primary">
[{t("custom")}]
</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 className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("assigment-type")} </Label> <Label>{t("assigment-type")} </Label>
<RadioGroup <RadioGroup
@ -793,21 +685,14 @@ export default function FormTaskTa() {
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("areas-expertise")}</Label> <Label>{t("areas-expertise")}</Label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{Object.keys(expertise).map((key) => ( {userCompetencies?.map((item: any) => (
<div className="flex items-center gap-2" key={key}> <div className="flex items-center gap-2" key={item.id}>
<Checkbox <Checkbox
id={key} id={`comp-${item.id}`}
checked={expertise[key as keyof typeof expertise]} checked={selectedCompetencies.has(item.id)}
onCheckedChange={(value) => onCheckedChange={() => handleCompetencyChange(item.id)}
handleExpertiseOutputChange(
key as keyof typeof expertise,
value as boolean
)
}
/> />
<Label htmlFor={key}> <Label htmlFor={`comp-${item.id}`}>{item.name}</Label>
{key.charAt(0).toUpperCase() + key.slice(1)}
</Label>
</div> </div>
))} ))}
</div> </div>
@ -815,23 +700,34 @@ export default function FormTaskTa() {
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">
<Label>{t("choose-expert")}</Label> <Label>{t("choose-expert")}</Label>
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{Object.keys(expert).map((key) => ( <Dialog>
<div className="flex items-center gap-2" key={key}> <DialogTrigger asChild>
<Checkbox <Button variant="soft" size="sm" color="primary">
id={key} [{"Pilih Tenaga Ahli"}]
checked={expert[key as keyof typeof expert]} </Button>
onCheckedChange={(value) => </DialogTrigger>
handleExpertOutputChange( <DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
key as keyof typeof expert, <DialogHeader>
value as boolean <DialogTitle>Daftar Tenaga Ahli</DialogTitle>
) </DialogHeader>
} <div className="grid grid-cols-2 gap-2 max-h-[400px] overflow-y-auto">
/> {listExpert?.map((expert: any) => (
<Label htmlFor={key}> <div key={expert.id} className="border p-2">
{key.charAt(0).toUpperCase() + key.slice(1)} <Label className="flex items-center">
</Label> <Checkbox
</div> checked={checkedLevels.has(expert.id)}
))} onCheckedChange={() =>
handleCheckboxChange(expert.id)
}
className="mr-3"
/>
{expert.fullname}
</Label>
</div>
))}
</div>
</DialogContent>
</Dialog>
</div> </div>
</div> </div>
<div className="mt-5 space-y-2"> <div className="mt-5 space-y-2">

View File

@ -47,8 +47,8 @@ const formWaveSurferOptions = (ref: any) => ({
barWidth: 3, barWidth: 3,
barRadius: 3, barRadius: 3,
responsive: true, responsive: true,
height: 150, // If true, normalize by the maximum peak instead of 1.0. height: 150,
normalize: true, // Use the PeakCache to improve rendering speed of large waveforms. normalize: true,
partialRender: true, partialRender: true,
}); });

View File

@ -2763,20 +2763,20 @@ export function getMenuList(pathname: string, t: any): Group[] {
icon: "uiw:user-delete", icon: "uiw:user-delete",
children: [], children: [],
}, },
{ // {
href: "/supervisor/communications/ptt", // href: "/supervisor/communications/ptt",
label: t("ptt"), // label: t("ptt"),
active: pathname.includes("/communications/ptt"), // active: pathname.includes("/communications/ptt"),
icon: "clarity:employee-group-line", // icon: "clarity:employee-group-line",
children: [], // children: [],
}, // },
{ // {
href: "/supervisor/communications/web-chat", // href: "/supervisor/communications/web-chat",
label: t("web-chat"), // label: t("web-chat"),
active: pathname.includes("/communications/web-chat"), // active: pathname.includes("/communications/web-chat"),
icon: "clarity:employee-group-line", // icon: "clarity:employee-group-line",
children: [], // children: [],
}, // },
], ],
}, },
], ],

View File

@ -53,6 +53,11 @@ export async function getMediaUpload(id: any, userLevelId: any) {
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function getMediaUploadTa(id: any, userLevelId: any) {
const url = `/assignment-expert/media-uploads?id=${id}&userLevelId=${userLevelId}`;
return httpGetInterceptor(url);
}
export async function forwardTask(data: any) { export async function forwardTask(data: any) {
const url = "assignment/forward"; const url = "assignment/forward";
return httpPostInterceptor(url, data); return httpPostInterceptor(url, data);
@ -63,11 +68,21 @@ export async function deleteTask(id: any) {
return httpDeleteInterceptor(url); return httpDeleteInterceptor(url);
} }
export async function deleteTaskTa(id: any) {
const url = `assignment-expert?id=${id}`;
return httpDeleteInterceptor(url);
}
export async function getUserLevelForAssignments() { export async function getUserLevelForAssignments() {
const url = "/users/user-levels/assignment"; const url = "/users/user-levels/assignment";
return httpGetInterceptor(url); return httpGetInterceptor(url);
} }
export async function getUserLevelForExpert(id: any) {
const url = `/users/assignment-expert?competencyIds=${id}`;
return httpGetInterceptor(url);
}
export async function getAssignmentResponseList(id: any) { export async function getAssignmentResponseList(id: any) {
const url = `assignment/response?assignmentId=${id}`; const url = `assignment/response?assignmentId=${id}`;
return httpGetInterceptor(url); return httpGetInterceptor(url);