feat: fixing error, etc
This commit is contained in:
parent
538d941976
commit
dab5958ac2
|
|
@ -1,7 +1,6 @@
|
|||
'use server'
|
||||
import { redirect } from "next/navigation";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { postMessage } from "@/app/[locale]/(protected)/app/chat/utils";
|
||||
|
||||
|
||||
export const postMessageAction = async (id: string, message: string,) => {
|
||||
|
|
|
|||
|
|
@ -54,22 +54,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./column";
|
||||
import { getPlanningPagination } from "@/service/agenda-setting/agenda-setting";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
getMediaBlastCampaignPage,
|
||||
listDataMedia,
|
||||
} from "@/service/broadcast/broadcast";
|
||||
import { listEnableCategory } from "@/service/content/content";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import { Link } from "@/i18n/routing";
|
||||
import { NewCampaignIcon } from "@/components/icon";
|
||||
import search from "../../../app/chat/components/search";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -79,7 +64,6 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||
import TablePagination from "@/components/table/table-pagination";
|
||||
import columns from "./columns";
|
||||
import { getPlanningSentPagination } from "@/service/planning/planning";
|
||||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useTableColumns from "./columns";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import FormTask from "@/components/form/task/task-form";
|
||||
import FormTaskRefactored from "@/components/form/task/task-form-refactored";
|
||||
|
||||
const TaskCreatePage = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
import { DockIcon, ImageIcon, MicIcon, YoutubeIcon } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
import search from "../../../app/chat/components/search";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type StatusFilter = string[];
|
||||
|
|
|
|||
|
|
@ -105,9 +105,7 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
|
|||
{t("member", { defaultValue: "Member" })} <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<select
|
||||
className={`py-2 px-3 rounded-md border text-sm border-slate-300 bg-white dark:bg-slate-600 ${
|
||||
errors.association ? "border-red-500" : ""
|
||||
}`}
|
||||
className={`py-2 px-3 rounded-md border text-sm border-slate-300 bg-white dark:bg-slate-600`}
|
||||
{...register("association" as keyof JournalistRegistrationData)}
|
||||
id="association"
|
||||
>
|
||||
|
|
@ -120,9 +118,9 @@ export const IdentityForm: React.FC<IdentityFormProps> = ({
|
|||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.association && (
|
||||
{/* {errors.association && (
|
||||
<div className="text-red-500 text-sm mt-1">{errors.association.message}</div>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
|
||||
<div className="px-0 lg:px-[34px] mb-4">
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
|
|||
}
|
||||
|
||||
const instituteData: InstituteData = {
|
||||
id: "0",
|
||||
name: customInstituteName,
|
||||
address: instituteAddress,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import "swiper/css/thumbs";
|
|||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
|
||||
import { files } from "@/app/[locale]/(protected)/app/projects/[id]/data";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import Image from "next/image";
|
||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import "swiper/css/thumbs";
|
|||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import { FreeMode, Navigation, Pagination, Thumbs } from "swiper/modules";
|
||||
import { files } from "@/app/[locale]/(protected)/app/projects/[id]/data";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import Image from "next/image";
|
||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
||||
|
|
|
|||
|
|
@ -1,840 +0,0 @@
|
|||
"use client";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
// Import new reusable form components
|
||||
import {
|
||||
FormField,
|
||||
FormSelect,
|
||||
FormCheckbox,
|
||||
FormRadio,
|
||||
FormSection,
|
||||
FormGrid,
|
||||
FormGridItem,
|
||||
SelectOption,
|
||||
CheckboxOption,
|
||||
RadioOption,
|
||||
} from "@/components/form/shared";
|
||||
|
||||
import {
|
||||
createTask,
|
||||
getTask,
|
||||
getUserLevelForAssignments,
|
||||
} from "@/service/task";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ChevronDown, ChevronUp, Trash2 } from "lucide-react";
|
||||
import { AudioRecorder } from "react-audio-voice-recorder";
|
||||
import FileUploader from "@/components/form/shared/file-uploader";
|
||||
import { Upload } from "tus-js-client";
|
||||
import { close, error } from "@/config/swal";
|
||||
import { getCsrfToken } from "@/service/auth";
|
||||
import { loading } from "@/lib/swal";
|
||||
import { useTranslations } from "next-intl";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
// =============================================================================
|
||||
// SCHEMA
|
||||
// =============================================================================
|
||||
|
||||
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.",
|
||||
}),
|
||||
assignmentSelection: z.string().min(1, { message: "Assignment selection is required" }),
|
||||
mainType: z.string().min(1, { message: "Main type is required" }),
|
||||
taskType: z.string().min(1, { message: "Task type is required" }),
|
||||
type: z.string().min(1, { message: "Type is required" }),
|
||||
taskOutput: z.array(z.string()).min(1, { message: "At least one output is required" }),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
interface FileWithPreview extends File {
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export type taskDetail = {
|
||||
id: number;
|
||||
title: string;
|
||||
fileTypeOutput: string;
|
||||
assignedToTopLevel: string;
|
||||
assignedToLevel: string;
|
||||
assignmentType: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
assignmentMainType: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
attachmentUrl: string;
|
||||
taskType: string;
|
||||
broadcastType: string;
|
||||
narration: string;
|
||||
is_active: string;
|
||||
};
|
||||
|
||||
const CustomEditor = dynamic(
|
||||
() => {
|
||||
return import("@/components/editor/custom-editor");
|
||||
},
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// OPTIONS DATA
|
||||
// =============================================================================
|
||||
|
||||
const assignmentSelectionOptions: SelectOption[] = [
|
||||
{ value: "3,4", label: "Semua Pengguna" },
|
||||
{ value: "4", label: "Kontributor" },
|
||||
{ value: "3", label: "Approver" },
|
||||
];
|
||||
|
||||
const mainTypeOptions: RadioOption[] = [
|
||||
{ value: "1", label: "Mediahub" },
|
||||
{ value: "2", label: "Medsos Mediahub" },
|
||||
];
|
||||
|
||||
const taskTypeOptions: RadioOption[] = [
|
||||
{ value: "atensi-khusus", label: "Atensi Khusus" },
|
||||
{ value: "tugas-harian", label: "Tugas Harian" },
|
||||
];
|
||||
|
||||
const typeOptions: RadioOption[] = [
|
||||
{ value: "1", label: "Publikasi" },
|
||||
{ value: "2", label: "Amplifikasi" },
|
||||
{ value: "3", label: "Kontra" },
|
||||
];
|
||||
|
||||
const taskOutputOptions: CheckboxOption[] = [
|
||||
{ value: "all", label: "All" },
|
||||
{ value: "video", label: "Video" },
|
||||
{ value: "audio", label: "Audio" },
|
||||
{ value: "image", label: "Image" },
|
||||
{ value: "text", label: "Text" },
|
||||
];
|
||||
|
||||
const unitSelectionOptions: CheckboxOption[] = [
|
||||
{ value: "allUnit", label: "All Unit" },
|
||||
{ value: "mabes", label: "Mabes" },
|
||||
{ value: "polda", label: "Polda" },
|
||||
{ value: "polres", label: "Polres" },
|
||||
{ value: "satker", label: "Satker" },
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export default function FormTaskRefactored() {
|
||||
const MySwal = withReactContent(Swal);
|
||||
const router = useRouter();
|
||||
const editor = useRef(null);
|
||||
type TaskSchema = z.infer<typeof taskSchema>;
|
||||
const { id } = useParams() as { id: string };
|
||||
const t = useTranslations("Form");
|
||||
|
||||
// =============================================================================
|
||||
// STATE
|
||||
// =============================================================================
|
||||
|
||||
const [detail, setDetail] = useState<taskDetail>();
|
||||
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 [isRecording, setIsRecording] = useState(false);
|
||||
const [timer, setTimer] = useState<number>(120);
|
||||
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 [voiceNoteLink, setVoiceNoteLink] = useState("");
|
||||
const [links, setLinks] = useState<string[]>([""]);
|
||||
|
||||
// =============================================================================
|
||||
// FORM SETUP
|
||||
// =============================================================================
|
||||
|
||||
const form = useForm<TaskSchema>({
|
||||
resolver: zodResolver(taskSchema),
|
||||
mode: "all",
|
||||
defaultValues: {
|
||||
title: detail?.title || "",
|
||||
naration: detail?.narration || "",
|
||||
assignmentSelection: "3,4",
|
||||
mainType: "1",
|
||||
taskType: "atensi-khusus",
|
||||
type: "1",
|
||||
taskOutput: [],
|
||||
},
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// EFFECTS
|
||||
// =============================================================================
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error("Error fetching Polda/Polres data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchPoldaPolres();
|
||||
}, []);
|
||||
|
||||
// =============================================================================
|
||||
// HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
const handleCheckboxChange = (levelId: number) => {
|
||||
setCheckedLevels((prev) => {
|
||||
const updatedLevels = new Set(prev);
|
||||
if (updatedLevels.has(levelId)) {
|
||||
updatedLevels.delete(levelId);
|
||||
} else {
|
||||
updatedLevels.add(levelId);
|
||||
}
|
||||
return updatedLevels;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePoldaPolresChange = () => {
|
||||
return Array.from(checkedLevels).join(",");
|
||||
};
|
||||
|
||||
const toggleExpand = (poldaId: any) => {
|
||||
setExpandedPolda((prev) => ({
|
||||
...prev,
|
||||
[poldaId]: !prev[poldaId],
|
||||
}));
|
||||
};
|
||||
|
||||
const onRecordingStart = () => {
|
||||
setIsRecording(true);
|
||||
setTimer(120);
|
||||
const interval = setInterval(() => {
|
||||
setTimer((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(interval);
|
||||
setIsRecording(false);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleStopRecording = () => {
|
||||
setIsRecording(false);
|
||||
};
|
||||
|
||||
const addAudioElement = (blob: Blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const audio = new Audio(url);
|
||||
const file = new File([blob], "voice-note.webm", { type: "audio/webm" });
|
||||
setAudioFile(file);
|
||||
setVoiceNoteLink(url);
|
||||
};
|
||||
|
||||
const handleDeleteAudio = (index: number) => {
|
||||
setAudioFiles((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleLinkChange = (index: number, value: string) => {
|
||||
const newLinks = [...links];
|
||||
newLinks[index] = value;
|
||||
setLinks(newLinks);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
setLinks([...links, ""]);
|
||||
};
|
||||
|
||||
const handleRemoveRow = (index: number) => {
|
||||
setLinks(links.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// SUBMISSION
|
||||
// =============================================================================
|
||||
|
||||
const save = async (data: TaskSchema) => {
|
||||
try {
|
||||
const csrfToken = await getCsrfToken();
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("title", data.title);
|
||||
formData.append("narration", data.naration);
|
||||
formData.append("assignmentSelection", data.assignmentSelection);
|
||||
formData.append("mainType", data.mainType);
|
||||
formData.append("taskType", data.taskType);
|
||||
formData.append("type", data.type);
|
||||
formData.append("taskOutput", data.taskOutput.join(","));
|
||||
formData.append("selectedTarget", handlePoldaPolresChange());
|
||||
|
||||
// Add file uploads
|
||||
if (videoFiles.length > 0) {
|
||||
videoFiles.forEach((file) => {
|
||||
formData.append("videoFiles", file);
|
||||
});
|
||||
}
|
||||
|
||||
if (imageFiles.length > 0) {
|
||||
imageFiles.forEach((file) => {
|
||||
formData.append("imageFiles", file);
|
||||
});
|
||||
}
|
||||
|
||||
if (textFiles.length > 0) {
|
||||
textFiles.forEach((file) => {
|
||||
formData.append("textFiles", file);
|
||||
});
|
||||
}
|
||||
|
||||
if (audioFiles.length > 0) {
|
||||
audioFiles.forEach((file) => {
|
||||
formData.append("audioFiles", file);
|
||||
});
|
||||
}
|
||||
|
||||
if (audioFile) {
|
||||
formData.append("audioFile", audioFile);
|
||||
}
|
||||
|
||||
// Add links
|
||||
formData.append("links", JSON.stringify(links.filter(link => link.trim() !== "")));
|
||||
|
||||
const response = await createTask(formData, csrfToken);
|
||||
|
||||
if (response?.data?.success) {
|
||||
successSubmit("/task");
|
||||
} else {
|
||||
error("Failed to create task");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving task:", error);
|
||||
error("An error occurred while saving the 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 successSubmit = (redirect: string) => {
|
||||
MySwal.fire({
|
||||
title: "Berhasil!",
|
||||
text: "Data berhasil disimpan",
|
||||
icon: "success",
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
}).then(() => {
|
||||
router.push(redirect);
|
||||
});
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// RENDER
|
||||
// =============================================================================
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Card className="shadow-lg border-0">
|
||||
<div className="px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
{t("form-task", { defaultValue: "Form Task" })} (Refactored)
|
||||
</h1>
|
||||
<p className="text-gray-600">Create and manage task assignments with detailed configuration</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* Basic Information */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200 pb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Enter the basic details for your task</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="title" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
{t("title", { defaultValue: "Title" })} *
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
{...form.register("title")}
|
||||
placeholder="Enter task title"
|
||||
className="h-12 px-4 text-base border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
{form.formState.errors.title && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.title.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assignment Configuration */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200 pb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Assignment Configuration</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Configure assignment settings and target audience</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label htmlFor="assignmentSelection" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
{t("assignment-selection", { defaultValue: "Assignment Selection" })} *
|
||||
</Label>
|
||||
<select
|
||||
id="assignmentSelection"
|
||||
{...form.register("assignmentSelection")}
|
||||
className="w-full h-12 px-4 text-base border border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500 bg-white"
|
||||
>
|
||||
<option value="">Choose assignment type</option>
|
||||
{assignmentSelectionOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{form.formState.errors.assignmentSelection && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.assignmentSelection.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700 mb-3 block">Unit Selection</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{unitSelectionOptions.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={option.value}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<Label htmlFor={option.value} className="text-sm text-gray-700">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom Assignment Dialog */}
|
||||
<div className="mt-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 px-4 border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
<span className="text-sm font-medium">Custom Assignment</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] md:max-w-[500px] lg:max-w-[1500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-lg font-semibold">Daftar Wilayah Polda dan Polres</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-2 gap-3 max-h-[400px] overflow-y-auto">
|
||||
{listDest.map((polda: any) => (
|
||||
<div key={polda.id} className="border border-gray-200 rounded-lg p-3">
|
||||
<Label className="flex items-center cursor-pointer">
|
||||
<Checkbox
|
||||
checked={checkedLevels.has(polda.id)}
|
||||
onCheckedChange={() => handleCheckboxChange(polda.id)}
|
||||
className="mr-3 h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm font-medium">{polda.name}</span>
|
||||
<button
|
||||
onClick={() => toggleExpand(polda.id)}
|
||||
className="ml-auto focus:outline-none p-1 hover:bg-gray-100 rounded"
|
||||
>
|
||||
{expandedPolda[polda.id] ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)}
|
||||
</button>
|
||||
</Label>
|
||||
{expandedPolda[polda.id] && (
|
||||
<div className="ml-6 mt-3 space-y-2">
|
||||
<Label className="flex items-center">
|
||||
<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 h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm">Pilih Semua Polres</span>
|
||||
</Label>
|
||||
{polda?.subDestination?.map((polres: any) => (
|
||||
<Label key={polres.id} className="flex items-center">
|
||||
<Checkbox
|
||||
checked={checkedLevels.has(polres.id)}
|
||||
onCheckedChange={() => handleCheckboxChange(polres.id)}
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
<span className="text-sm">{polres.name}</span>
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Configuration */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200 pb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Task Configuration</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Configure task type and output settings</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700 mb-3 block">
|
||||
{t("type-task", { defaultValue: "Type Task" })} *
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{mainTypeOptions.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`mainType-${option.value}`}
|
||||
{...form.register("mainType")}
|
||||
value={option.value}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
<Label htmlFor={`mainType-${option.value}`} className="text-sm text-gray-700">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{form.formState.errors.mainType && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.mainType.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700 mb-3 block">
|
||||
{t("assigment-type", { defaultValue: "Assignment Type" })} *
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{taskTypeOptions.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`taskType-${option.value}`}
|
||||
{...form.register("taskType")}
|
||||
value={option.value}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
<Label htmlFor={`taskType-${option.value}`} className="text-sm text-gray-700">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{form.formState.errors.taskType && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.taskType.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700 mb-3 block">
|
||||
{t("type-of-task", { defaultValue: "Type Of Task" })} *
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{typeOptions.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`type-${option.value}`}
|
||||
{...form.register("type")}
|
||||
value={option.value}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 focus:ring-blue-500"
|
||||
/>
|
||||
<Label htmlFor={`type-${option.value}`} className="text-sm text-gray-700">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{form.formState.errors.type && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-sm font-medium text-gray-700 mb-3 block">
|
||||
{t("output-task", { defaultValue: "Output Task" })} *
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{taskOutputOptions.map((option) => (
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`taskOutput-${option.value}`}
|
||||
{...form.register("taskOutput")}
|
||||
value={option.value}
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<Label htmlFor={`taskOutput-${option.value}`} className="text-sm text-gray-700">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{form.formState.errors.taskOutput && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.taskOutput.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task Description */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200 pb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Task Description</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Provide detailed description of the task</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="naration" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
{t("description", { defaultValue: "Description" })} *
|
||||
</Label>
|
||||
<div className="border border-gray-300 rounded-md focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500">
|
||||
<CustomEditor
|
||||
onChange={form.register("naration").onChange}
|
||||
initialData={form.watch("naration")}
|
||||
/>
|
||||
</div>
|
||||
{form.formState.errors.naration && (
|
||||
<p className="text-red-500 text-sm mt-1">{form.formState.errors.naration.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
<div className="space-y-4">
|
||||
<div className="border-b border-gray-200 pb-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Attachments</h2>
|
||||
<p className="text-sm text-gray-600 mt-1">Upload files and add links for the task</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* File Uploaders */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
{t("audio-visual", { defaultValue: "Audio Visual" })}
|
||||
</Label>
|
||||
<FileUploader
|
||||
accept={{ "video/*": [] }}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .mp4 atau .mov."
|
||||
onDrop={(files) => setVideoFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
{t("image", { defaultValue: "Image" })}
|
||||
</Label>
|
||||
<FileUploader
|
||||
accept={{ "image/*": [] }}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .png, .jpg, atau .jpeg."
|
||||
onDrop={(files) => setImageFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
{t("text", { defaultValue: "Text" })}
|
||||
</Label>
|
||||
<FileUploader
|
||||
accept={{ "application/pdf": [] }}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .pdf."
|
||||
onDrop={(files) => setTextFiles(files)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
{t("audio", { defaultValue: "Audio" })}
|
||||
</Label>
|
||||
<AudioRecorder
|
||||
onRecordingComplete={addAudioElement}
|
||||
audioTrackConstraints={{
|
||||
noiseSuppression: true,
|
||||
echoCancellation: true,
|
||||
}}
|
||||
downloadOnSavePress={true}
|
||||
downloadFileExtension="webm"
|
||||
/>
|
||||
<FileUploader
|
||||
accept={{ "audio/*": [] }}
|
||||
maxSize={100}
|
||||
label="Upload file dengan format .mp3 atau .wav."
|
||||
onDrop={(files) => setAudioFiles((prev) => [...prev, ...files])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audio Files List */}
|
||||
{audioFiles?.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-gray-700">Uploaded Audio Files</Label>
|
||||
{audioFiles.map((audio: any, idx: any) => (
|
||||
<div key={idx} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<span className="text-sm text-gray-700">{t("voice-note", { defaultValue: "Voice Note" })} {idx + 1}</span>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => handleDeleteAudio(idx)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRecording && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-700">Recording... {timer} seconds remaining</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* News Links */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium text-gray-700">
|
||||
{t("news-links", { defaultValue: "News Links" })}
|
||||
</Label>
|
||||
<div className="space-y-3">
|
||||
{links.map((link, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<Input
|
||||
type="url"
|
||||
className="flex-1 h-11"
|
||||
placeholder={`Masukkan link berita ${index + 1}`}
|
||||
value={link}
|
||||
onChange={(e) => handleLinkChange(index, e.target.value)}
|
||||
/>
|
||||
{links.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveRow(index)}
|
||||
className="h-11 w-11 p-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleAddRow}
|
||||
className="h-10 px-4 border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
<span className="text-sm font-medium">+ Add Link</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="h-12 px-8 text-base font-medium bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{t("submit", { defaultValue: "Submit" })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import router from "next/router";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import router from "next/router";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import router from "next/router";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectItem } from "@radix-ui/react-select";
|
||||
import { Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectItem } from "@radix-ui/react-select";
|
||||
import { Icon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import search from "@/app/[locale]/(protected)/app/chat/components/search";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ export const sanitizeRegistrationData = (data: RegistrationFormData): Registrati
|
|||
|
||||
export const sanitizeInstituteData = (data: InstituteData): InstituteData => {
|
||||
return {
|
||||
id : data.id,
|
||||
name: data.name.trim(),
|
||||
address: data.address.trim(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import Cookies from "js-cookie";
|
|||
import CryptoJS from "crypto-js";
|
||||
import Swal from "sweetalert2";
|
||||
import withReactContent from "sweetalert2-react-content";
|
||||
import Loading from "@/app/[locale]/(protected)/app/projects/loading";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export const generalRegistrationSchema = z.object({
|
|||
});
|
||||
|
||||
export const instituteSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { message: "Institute name is required" })
|
||||
|
|
|
|||
Loading…
Reference in New Issue