840 lines
32 KiB
TypeScript
840 lines
32 KiB
TypeScript
"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>
|
|
);
|
|
}
|