add edit faq, edit feedback and edit lowongan
This commit is contained in:
@@ -25,6 +25,9 @@ import AddFeedback from "./Pages/AddFeedback";
|
|||||||
import ListFaq from "./Pages/ListFaq";
|
import ListFaq from "./Pages/ListFaq";
|
||||||
import AddFaq from "./Pages/AddFAQ";
|
import AddFaq from "./Pages/AddFAQ";
|
||||||
import Dashboard from "./Pages/Dashboard";
|
import Dashboard from "./Pages/Dashboard";
|
||||||
|
import EditFaq from "./Pages/EditFAQ";
|
||||||
|
import EditFeedback from "./Pages/EditFeedback";
|
||||||
|
import EditLowongan from "./Pages/EditLowongan";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -61,6 +64,9 @@ function App() {
|
|||||||
<Route path="/faq-list" element={<ListFaq />} />
|
<Route path="/faq-list" element={<ListFaq />} />
|
||||||
<Route path="/add-faq" element={<AddFaq />} />
|
<Route path="/add-faq" element={<AddFaq />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/edit-faq/:id" element={<EditFaq/>}></Route>
|
||||||
|
<Route path="/edit-feedback/:id" element={<EditFeedback/>}></Route>
|
||||||
|
<Route path="/edit-lowongan/:id" element={<EditLowongan/>}></Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||||
|
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||||
|
import "froala-editor/js/plugins.pkgd.min.js";
|
||||||
|
import "froala-editor/css/froala_editor.pkgd.min.css";
|
||||||
|
import "froala-editor/css/froala_style.min.css";
|
||||||
|
import FroalaEditor from "react-froala-wysiwyg";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const EditFaq = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const [questions, setQuestions] = useState("");
|
||||||
|
const [answers, setAnswers] = useState("<p></p>");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [prefilled, setPrefilled] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFaqDetail = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setPrefilled(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.get(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/faq-api/get/${id}`,
|
||||||
|
{ headers: token ? { Authorization: `Bearer ${token}` } : undefined }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data?.status) {
|
||||||
|
const data = res.data.data;
|
||||||
|
setQuestions(data?.pertanyaan ?? "");
|
||||||
|
setAnswers(data?.jawaban ?? "<p></p>");
|
||||||
|
setPrefilled(true);
|
||||||
|
} else {
|
||||||
|
alert(res.data?.message || "Gagal mengambil detail FAQ.");
|
||||||
|
navigate("/faq-list");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
alert(e?.response?.data?.message || "Gagal mengambil detail FAQ.");
|
||||||
|
navigate("/faq-list");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFaqDetail();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!token) return alert("Token tidak ditemukan.");
|
||||||
|
if (!id) return alert("ID FAQ tidak ditemukan.");
|
||||||
|
if (!questions || !answers) return alert("Semua field wajib diisi.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.patch(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/faq-api/update/${id}`,
|
||||||
|
{ pertanyaan: questions, jawaban: answers },
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data?.status) {
|
||||||
|
alert("Faq berhasil diperbarui!");
|
||||||
|
navigate("/faq-list");
|
||||||
|
} else {
|
||||||
|
alert(res.data?.message || "Gagal memperbarui faq.");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
alert(
|
||||||
|
e?.response?.data?.message || "Terjadi kesalahan saat memperbarui faq."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full overflow-hidden">
|
||||||
|
<NavbarAdmin />
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<SidebarAdmin />
|
||||||
|
<div className="p-10 bg-white flex-col flex-wrap w-full">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h2 className="text-xl font-semibold">
|
||||||
|
Edit Frequently Asked Questions
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium disabled:opacity-60 disabled:cursor-not-allowed"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Loading..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="flex flex-col gap-6 max-w-full">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Questions</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={questions}
|
||||||
|
onChange={(e) => setQuestions(e.target.value)}
|
||||||
|
placeholder="Tulis pertanyaan kamu..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Answers</label>
|
||||||
|
|
||||||
|
{/* Render Froala hanya setelah mount + data terisi */}
|
||||||
|
{mounted && prefilled ? (
|
||||||
|
<FroalaEditor
|
||||||
|
key={id}
|
||||||
|
tag="textarea"
|
||||||
|
model={answers}
|
||||||
|
onModelChange={(model: any) => setAnswers(model ?? "<p></p>")}
|
||||||
|
config={{
|
||||||
|
placeholderText: "Tulis jawaban kamu...",
|
||||||
|
charCounterCount: true,
|
||||||
|
toolbarSticky: true,
|
||||||
|
heightMin: 200,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="border border-gray-300 rounded-lg p-3 text-gray-500">
|
||||||
|
{loading ? "Memuat editor..." : "Menyiapkan editor..."}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFaq;
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||||
|
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||||
|
import "froala-editor/js/plugins.pkgd.min.js";
|
||||||
|
import "froala-editor/css/froala_editor.pkgd.min.css";
|
||||||
|
import "froala-editor/css/froala_style.min.css";
|
||||||
|
import FroalaEditor from "react-froala-wysiwyg";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type FeedbackDetailResponse = {
|
||||||
|
status: boolean;
|
||||||
|
data?: {
|
||||||
|
id?: string | number;
|
||||||
|
nama?: string;
|
||||||
|
universitas?: string;
|
||||||
|
posisi?: string;
|
||||||
|
batch?: string | number;
|
||||||
|
tahun?: string | number;
|
||||||
|
pesan?: string;
|
||||||
|
image_path?: string; // opsional kalau BE ngasih path gambar lama
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditFeedback = () => {
|
||||||
|
const { id } = useParams(); // /feedback-edit/:id (contoh)
|
||||||
|
const [posisi, setPosisi] = useState("");
|
||||||
|
const [universitas, setUniversitas] = useState("");
|
||||||
|
const [nama, setNama] = useState("");
|
||||||
|
const [image, setImage] = useState<File | null>(null);
|
||||||
|
const [batch, setBatch] = useState("");
|
||||||
|
const [feedback, setFeedbacks] = useState("");
|
||||||
|
const [year, setYear] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [existingImagePath, setExistingImagePath] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
alert("Token tidak ditemukan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
alert("ID feedback tidak ditemukan.");
|
||||||
|
navigate("/feedback-list");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<FeedbackDetailResponse>(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/get/${id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.status && response.data.data) {
|
||||||
|
const d = response.data.data;
|
||||||
|
|
||||||
|
setNama(d.nama ?? "");
|
||||||
|
setUniversitas(d.universitas ?? "");
|
||||||
|
setPosisi(d.posisi ?? "");
|
||||||
|
setBatch(d.batch !== undefined ? String(d.batch) : "");
|
||||||
|
setYear(d.tahun !== undefined ? String(d.tahun) : "");
|
||||||
|
setFeedbacks(d.pesan ?? "");
|
||||||
|
setExistingImagePath(d.image_path ?? "");
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || "Gagal mengambil detail feedback.");
|
||||||
|
navigate("/feedback-list");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error fetching feedback detail:", error);
|
||||||
|
alert("Terjadi kesalahan saat mengambil detail feedback.");
|
||||||
|
navigate("/feedback-list");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDetail();
|
||||||
|
}, [id, navigate]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
alert("Token tidak ditemukan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
alert("ID feedback tidak ditemukan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nama || !universitas || !posisi || !batch || !year || !feedback) {
|
||||||
|
alert("Semua field wajib diisi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("nama", nama);
|
||||||
|
formData.append("universitas", universitas);
|
||||||
|
formData.append("posisi", posisi);
|
||||||
|
formData.append("batch", batch);
|
||||||
|
formData.append("tahun", year);
|
||||||
|
formData.append("pesan", feedback);
|
||||||
|
|
||||||
|
if (image) formData.append("image", image);
|
||||||
|
|
||||||
|
if (!image && existingImagePath) {
|
||||||
|
formData.append("existing_image_path", existingImagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.patch(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/feedback-api/update/${id}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.status) {
|
||||||
|
alert("Feedback berhasil diupdate!");
|
||||||
|
navigate("/feedback-list");
|
||||||
|
} else {
|
||||||
|
alert(response.data.message || "Gagal mengupdate feedback.");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Error updating feedback:", error);
|
||||||
|
alert("Terjadi kesalahan saat mengupdate feedback.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full overflow-hidden">
|
||||||
|
<NavbarAdmin />
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<SidebarAdmin />
|
||||||
|
<div className="p-10 bg-white flex-col flex-wrap w-full">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h2 className="text-xl font-semibold">Edit Feedback</h2>
|
||||||
|
<button
|
||||||
|
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="flex flex-col gap-6 max-w-full">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Nama</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={nama}
|
||||||
|
onChange={(e) => setNama(e.target.value)}
|
||||||
|
placeholder="John Doe"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Universitas</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={universitas}
|
||||||
|
onChange={(e) => setUniversitas(e.target.value)}
|
||||||
|
placeholder="Telkom University Bandung"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Posisi</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={posisi}
|
||||||
|
onChange={(e) => setPosisi(e.target.value)}
|
||||||
|
placeholder="UI/UX Designer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Batch</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={batch}
|
||||||
|
onChange={(e) => setBatch(e.target.value)}
|
||||||
|
placeholder="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Tahun</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => setYear(e.target.value)}
|
||||||
|
placeholder="2025"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 col-span-2">
|
||||||
|
<label className="text-sm font-medium">Image anda</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) =>
|
||||||
|
setImage(e.target.files ? e.target.files[0] : null)
|
||||||
|
}
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Isi Konten */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Isi Konten</label>
|
||||||
|
<FroalaEditor
|
||||||
|
tag="textarea"
|
||||||
|
model={feedback}
|
||||||
|
onModelChange={(model: any) => setFeedbacks(model)}
|
||||||
|
config={{
|
||||||
|
placeholderText: "Tulis feedback kamu...",
|
||||||
|
charCounterCount: true,
|
||||||
|
toolbarSticky: true,
|
||||||
|
heightMin: 200,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFeedback;
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import SidebarAdmin from "../Layout/SidebarAdmin";
|
||||||
|
import NavbarAdmin from "../Layout/NavbarAdmin";
|
||||||
|
import "froala-editor/js/plugins.pkgd.min.js";
|
||||||
|
import "froala-editor/css/froala_editor.pkgd.min.css";
|
||||||
|
import "froala-editor/css/froala_style.min.css";
|
||||||
|
import FroalaEditor from "react-froala-wysiwyg";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
type LowonganDetail = {
|
||||||
|
id: number;
|
||||||
|
posisi: string;
|
||||||
|
kelompok_peminatan: string;
|
||||||
|
lokasi: string;
|
||||||
|
paid: "paid" | "unpaid" | string;
|
||||||
|
durasi_awal: string;
|
||||||
|
durasi_akhir: string;
|
||||||
|
jobdesk: string;
|
||||||
|
image?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditLowongan = () => {
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [posisi, setPosisi] = useState("");
|
||||||
|
const [kelompok, setKelompok] = useState("");
|
||||||
|
const [kelompokOptions, setKelompokOptions] = useState<string[]>([]);
|
||||||
|
const [lokasi, setLokasi] = useState("");
|
||||||
|
const [paid, setPaid] = useState("paid");
|
||||||
|
const [durasiAwal, setDurasiAwal] = useState("");
|
||||||
|
const [durasiAkhir, setDurasiAkhir] = useState("");
|
||||||
|
const [jobdesk, setJobdesk] = useState<string>("");
|
||||||
|
const [image, setImage] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const [isCustomKelompok, setIsCustomKelompok] = useState(false);
|
||||||
|
const [customKelompok, setCustomKelompok] = useState("");
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const [froalaReady, setFroalaReady] = useState(false);
|
||||||
|
|
||||||
|
const token = document.cookie
|
||||||
|
.split("; ")
|
||||||
|
.find((row) => row.startsWith("token="))
|
||||||
|
?.split("=")[1];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_API_BASE_URL
|
||||||
|
}/lowongan-magang-api/get/kelompok-all`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
const data = res.data.data || [];
|
||||||
|
setKelompokOptions(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Gagal memuat kelompok peminatan:", err);
|
||||||
|
setKelompokOptions([]);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
if (!id) {
|
||||||
|
alert("ID lowongan tidak ditemukan.");
|
||||||
|
navigate("/lowongan-list");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
alert("Token tidak ditemukan.");
|
||||||
|
navigate("/humic-20242025-2");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setFroalaReady(false);
|
||||||
|
|
||||||
|
const res = await axios.get(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/get/id/${id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data: LowonganDetail = res.data.data;
|
||||||
|
|
||||||
|
setPosisi(data.posisi ?? "");
|
||||||
|
setLokasi(data.lokasi ?? "");
|
||||||
|
setPaid((data.paid ?? "paid") as string);
|
||||||
|
setDurasiAwal((data.durasi_awal ?? "").slice(0, 10));
|
||||||
|
setDurasiAkhir((data.durasi_akhir ?? "").slice(0, 10));
|
||||||
|
setJobdesk(String(data.jobdesk ?? ""));
|
||||||
|
|
||||||
|
const kp = data.kelompok_peminatan ?? "";
|
||||||
|
setKelompok(kp);
|
||||||
|
setCustomKelompok(kp);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Gagal memuat detail lowongan:", err);
|
||||||
|
alert("Gagal memuat detail lowongan.");
|
||||||
|
navigate("/lowongan-list");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDetail();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
const t = setTimeout(() => setFroalaReady(true), 0);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!kelompok) return;
|
||||||
|
|
||||||
|
if (kelompokOptions.length === 0) {
|
||||||
|
setIsCustomKelompok(true);
|
||||||
|
setCustomKelompok(kelompok);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = kelompokOptions.includes(kelompok);
|
||||||
|
if (exists) {
|
||||||
|
setIsCustomKelompok(false);
|
||||||
|
setCustomKelompok("");
|
||||||
|
} else {
|
||||||
|
setIsCustomKelompok(true);
|
||||||
|
setCustomKelompok(kelompok);
|
||||||
|
}
|
||||||
|
}, [kelompokOptions, kelompok]);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!id) {
|
||||||
|
alert("ID lowongan tidak ditemukan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
alert("Token tidak ditemukan.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalKelompok =
|
||||||
|
isCustomKelompok || kelompokOptions.length === 0
|
||||||
|
? customKelompok
|
||||||
|
: kelompok;
|
||||||
|
|
||||||
|
if (!finalKelompok.trim()) {
|
||||||
|
alert("Kategori internship wajib diisi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("posisi", posisi);
|
||||||
|
formData.append("kelompok_peminatan", finalKelompok);
|
||||||
|
formData.append("lokasi", lokasi);
|
||||||
|
formData.append("jobdesk", jobdesk);
|
||||||
|
formData.append("durasi_awal", durasiAwal);
|
||||||
|
formData.append("durasi_akhir", durasiAkhir);
|
||||||
|
formData.append("paid", paid);
|
||||||
|
|
||||||
|
if (image) formData.append("image", image);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.patch(
|
||||||
|
`${import.meta.env.VITE_API_BASE_URL}/lowongan-magang-api/update/${id}`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
alert("Lowongan berhasil diperbarui!");
|
||||||
|
navigate("/lowongan-list");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating internship:", error);
|
||||||
|
alert("Gagal memperbarui lowongan.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen w-full overflow-hidden">
|
||||||
|
<NavbarAdmin />
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<SidebarAdmin />
|
||||||
|
<div className="p-10 bg-white flex-col flex-wrap w-full">
|
||||||
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
<h2 className="text-xl font-semibold">Edit Lowongan</h2>
|
||||||
|
<button
|
||||||
|
className="bg-[#1F4A92] hover:bg-[#677c9f] cursor-pointer text-white px-6 py-2 rounded-md font-medium"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isLoading}
|
||||||
|
title={isLoading ? "Memuat data..." : "Submit"}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="flex flex-col gap-6 max-w-full">
|
||||||
|
{/* Internship Fields Only */}
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Kategori Internship
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{kelompokOptions.length > 0 && (
|
||||||
|
<select
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={isCustomKelompok ? "lainnya" : kelompok}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "lainnya") {
|
||||||
|
setIsCustomKelompok(true);
|
||||||
|
setCustomKelompok("");
|
||||||
|
} else {
|
||||||
|
setKelompok(value);
|
||||||
|
setIsCustomKelompok(false);
|
||||||
|
setCustomKelompok("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{kelompokOptions.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value="lainnya">Lainnya (masukan manual)</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isCustomKelompok || kelompokOptions.length === 0) && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3 mt-2"
|
||||||
|
placeholder="Tulis kategori internship baru"
|
||||||
|
value={customKelompok}
|
||||||
|
onChange={(e) => setCustomKelompok(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Paid</label>
|
||||||
|
<select
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={paid}
|
||||||
|
onChange={(e) => setPaid(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="paid">Paid</option>
|
||||||
|
<option value="unpaid">Unpaid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Lokasi</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={lokasi}
|
||||||
|
onChange={(e) => setLokasi(e.target.value)}
|
||||||
|
placeholder="Onsite,Jakarta"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Nama Internship</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={posisi}
|
||||||
|
onChange={(e) => setPosisi(e.target.value)}
|
||||||
|
placeholder="UI/UX Designer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Tanggal Mulai Oprec
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={durasiAwal}
|
||||||
|
onChange={(e) => setDurasiAwal(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Tanggal Akhir Oprec
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
value={durasiAkhir}
|
||||||
|
onChange={(e) => setDurasiAkhir(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 col-span-2">
|
||||||
|
<label className="text-sm font-medium">Thumbnail / Image</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) =>
|
||||||
|
setImage(e.target.files ? e.target.files[0] : null)
|
||||||
|
}
|
||||||
|
className="border border-gray-300 rounded-lg p-3"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Isi Konten */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-medium">Isi Konten</label>
|
||||||
|
|
||||||
|
{froalaReady ? (
|
||||||
|
<FroalaEditor
|
||||||
|
key={id}
|
||||||
|
tag="textarea"
|
||||||
|
model={jobdesk || ""}
|
||||||
|
onModelChange={(model: any) => setJobdesk(model || "")}
|
||||||
|
config={{
|
||||||
|
placeholderText: "Tulis deskripsi lowongan...",
|
||||||
|
charCounterCount: true,
|
||||||
|
toolbarSticky: true,
|
||||||
|
heightMin: 200,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="border border-gray-300 rounded-lg p-3 h-[200px]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditLowongan;
|
||||||
@@ -144,7 +144,7 @@ const ListFeedback = () => {
|
|||||||
<button
|
<button
|
||||||
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
|
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(`/edit-product-humic/${feedback.id}`)
|
navigate(`/edit-feedback/${feedback.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -151,6 +151,34 @@ const ListLowongan = () => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Tombol Edit */}
|
||||||
|
<button
|
||||||
|
className="bg-[#2CAEFF] text-white cursor-pointer p-2 rounded-xl"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/edit-lowongan/${low.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M19.5 7.125 18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Tombol Hapus */}
|
{/* Tombol Hapus */}
|
||||||
<button
|
<button
|
||||||
className="bg-[#E41E1E] text-white p-2 rounded-xl cursor-pointer"
|
className="bg-[#E41E1E] text-white p-2 rounded-xl cursor-pointer"
|
||||||
|
|||||||
Reference in New Issue
Block a user