feat: implement profile settings and comprehensive mentor-intern project management system

This commit is contained in:
Kemas M. Aryadary Rasyad
2026-07-10 10:01:25 +07:00
parent 88f457258d
commit 17e3928d35
30 changed files with 302 additions and 278 deletions
+3 -3
View File
@@ -41,7 +41,7 @@ import MentorDetailProject from "./Pages/Internify/mentor/ProjectsPage";
import InternifyLogin from "./Pages/Internify/LoginInternify" import InternifyLogin from "./Pages/Internify/LoginInternify"
import FAQPage from "./Pages/Internify/FAQpage"; import FAQPage from "./Pages/Internify/FAQpage";
import NotificationList from "./Pages/Internify/NotificationList"; import NotificationList from "./Pages/Internify/NotificationList";
import SettingsContent from "./Pages/Internify/SettingsPage"; import SettingsPage from "./Pages/Internify/SettingsPage";
import TaskSubmission from "./Pages/Internify/intern/TaskSubmission"; import TaskSubmission from "./Pages/Internify/intern/TaskSubmission";
import SertificateList from "./Pages/Internify/intern/InternCertList"; import SertificateList from "./Pages/Internify/intern/InternCertList";
import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage"; import MentorProjectsDetailPage from "./Pages/Internify/mentor/MentorProjectDetailPage";
@@ -116,7 +116,7 @@ function App() {
<Route path="projects/:slug/:taskSlug" element={<TaskSubmission />} /> <Route path="projects/:slug/:taskSlug" element={<TaskSubmission />} />
<Route path="faq" element={<FAQPage />} /> <Route path="faq" element={<FAQPage />} />
<Route path="notifications" element={<NotificationList />} /> <Route path="notifications" element={<NotificationList />} />
<Route path="settings" element={<SettingsContent />} /> <Route path="settings" element={<SettingsPage />} />
</Route> </Route>
</Route> </Route>
@@ -134,7 +134,7 @@ function App() {
<Route path="projects/:slug/:taskSlug/:nameIntern" element={<ViewInternSubmission />} /> <Route path="projects/:slug/:taskSlug/:nameIntern" element={<ViewInternSubmission />} />
<Route path="faq" element={<FAQPage />} /> <Route path="faq" element={<FAQPage />} />
<Route path="notifications" element={<NotificationList />} /> <Route path="notifications" element={<NotificationList />} />
<Route path="settings" element={<SettingsContent />} /> <Route path="settings" element={<SettingsPage />} />
</Route> </Route>
</Route> </Route>
+1 -6
View File
@@ -4,10 +4,6 @@ import humiclogo from "../assets/logo.png";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useCurrentUser } from "../hooks/useUser"; import { useCurrentUser } from "../hooks/useUser";
/** Singkatan nama: ambil huruf pertama setiap kata, maksimal 2 huruf.
* "Andi Mahesa" → "AM"
* "Andi Bayu Cendika" → "AC" (huruf pertama + huruf pertama kata terakhir)
*/
function getInitials(fullName: string | undefined | null): string { function getInitials(fullName: string | undefined | null): string {
if (!fullName) return "U"; if (!fullName) return "U";
const parts = fullName.trim().split(/\s+/).filter(Boolean); const parts = fullName.trim().split(/\s+/).filter(Boolean);
@@ -25,9 +21,8 @@ export default function Header({ toggleSidebar }: { toggleSidebar: () => void })
setImageError(false); setImageError(false);
}, [user?.profile_picture, user?.full_name]); }, [user?.profile_picture, user?.full_name]);
// Tentukan base path settings sesuai role
const settingsPath = const settingsPath =
user?.role === "intern" ? "/intern/settings" : "/mentor/settings"; user?.role === "intern" ? "/intern" : "/mentor";
const initials = user?.full_name ? getInitials(user.full_name) : "U"; const initials = user?.full_name ? getInitials(user.full_name) : "U";
const hasPhoto = !!user?.profile_picture && !imageError; const hasPhoto = !!user?.profile_picture && !imageError;
+143 -129
View File
@@ -86,26 +86,6 @@ type ApplicationsListResponse = {
code?: number; code?: number;
}; };
//type baru untuk menyesuaikan perubahan pada batch
type LamaranItem = {
id: number;
id_mahasiswa: number;
id_lowongan_magang: string;
status: string;
batch: number;
batch_data: { id: number; batch_number: number; is_active: boolean };
lowongan_magang: { posisi: string; kelompok_peminatan: string };
mahasiswa: { nama_depan: string; nama_belakang: string };
created_at: string;
update_at: string;
};
type LamaranListResponse = {
status: boolean;
data: LamaranItem[];
message?: string;
};
const CATEGORY_COLORS = [ const CATEGORY_COLORS = [
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
@@ -133,9 +113,6 @@ function groupCount(list: Array<string>) {
.sort((a, b) => b.value - a.value); .sort((a, b) => b.value - a.value);
} }
const EMPTY_PIE = [{ name: "Tidak ada data", value: 1 }];
const EMPTY_COLOR = ["#e5e7eb"]
function PieLegend({ function PieLegend({
data, data,
colors, colors,
@@ -143,11 +120,6 @@ function PieLegend({
data: ChartDataItem[]; data: ChartDataItem[];
colors: string[]; colors: string[];
}) { }) {
if (data.length === 0) {
return (
<p className="text-center text-sm text-gray-400">Tidak ada data</p>
)
}
return ( return (
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
{data.map((item, index) => ( {data.map((item, index) => (
@@ -185,9 +157,7 @@ const Dashboard: React.FC = () => {
const [batches, setBatches] = useState<BatchResponse["data"]>([]); const [batches, setBatches] = useState<BatchResponse["data"]>([]);
const [selectedBatchId, setSelectedBatchId] = useState<number | null>(null); const [selectedBatchId, setSelectedBatchId] = useState<number | null>(null);
const [batchDropdownOpen, setBatchDropdownOpen] = useState(false); const [batchDropdownOpen, setBatchDropdownOpen] = useState(false);
const selectedBatch = batches.find((b) => b.id === selectedBatchId) ?? null;
const selectedBatch =
batches.find((b) => b.id === selectedBatchId) ?? null;
const getTokenFromCookie = () => { const getTokenFromCookie = () => {
return document.cookie return document.cookie
@@ -235,10 +205,11 @@ const Dashboard: React.FC = () => {
return msgFromBe || `Request gagal (status ${status ?? "unknown"}).`; return msgFromBe || `Request gagal (status ${status ?? "unknown"}).`;
}; };
// Effect 1: ambil daftar batch sekali, set default = batch aktif
useEffect(() => { useEffect(() => {
const fetchBatches = async () => { const fetchBatches = async () => {
try { try {
const res = await api.get<BatchResponse>("/batch-api/"); const res = await api.get<BatchResponse>("/batch-api");
if (res.data?.status) { if (res.data?.status) {
const list = res.data.data || []; const list = res.data.data || [];
setBatches(list); setBatches(list);
@@ -258,9 +229,7 @@ const Dashboard: React.FC = () => {
useEffect(() => { useEffect(() => {
if (selectedBatchId === null) return; if (selectedBatchId === null) return;
const batch = batches.find((b) => b.id === selectedBatchId); const batchId = selectedBatchId;
if (!batch) return;
const batchNumber = batch.batch_number;
const fetchDashboardData = async () => { const fetchDashboardData = async () => {
try { try {
@@ -274,95 +243,142 @@ const Dashboard: React.FC = () => {
return; return;
} }
const batchParams = { params: { batch: batchNumber } }; const params = { batch_id: batchId };
const handle404 = (err: unknown) => { const [statsRes, positionRes, countryRes, appsRes] =
if (axios.isAxiosError(err) && err.response?.status === 404) { await Promise.all([
return { data: { status: true, data: [] } } as any; api.get<DashboardStatsResponse>(
} "/lamaran-magang-api/statistics/dashboard", { params }
throw err; ),
}; api.get<PositionStatsResponse>(
"/lamaran-magang-api/statistics/position", { params }
),
api.get<CountryStatsResponse>(
"/lamaran-magang-api/statistics/country", { params }
),
api.get<ApplicationsListResponse>("/lamaran-magang-api/statistics/university", { params }),
]);
const [lamaranRes, countryRes, uniRes] = await Promise.all([ const statsJson = statsRes.data;
api.get<LamaranListResponse>("/lamaran-magang-api/get", { const positionJson = positionRes.data;
params: { limit: 9999, page: 1 }, const countryJson = countryRes.data;
}), const appsJson = appsRes.data;
api
.get<CountryStatsResponse>("/lamaran-magang-api/statistics/country", batchParams)
.catch(handle404),
api
.get<ApplicationsListResponse>("/lamaran-magang-api/statistics/university", batchParams)
.catch(handle404),
]);
const all = lamaranRes.data?.data ?? []; if (!statsJson?.status) {
setErrorMessage(
statsJson?.message ||
"Gagal mengambil data dashboard (statistics/dashboard)."
);
setLoading(false);
return;
}
// filter manual per batch if (!positionJson?.status) {
const rows = all.filter((r) => Number(r.batch) === Number(batchNumber)); setErrorMessage(
positionJson?.message ||
"Gagal mengambil data dashboard (statistics/position)."
);
setLoading(false);
return;
}
const norm = (s?: string) => (s ?? "").toLowerCase().trim(); if (!countryJson?.status) {
setErrorMessage(
countryJson?.message ||
"Gagal mengambil data dashboard (statistics/country)."
);
setLoading(false);
return;
}
if (!appsJson?.status) {
console.warn(
"Fetch applicants list gagal (untuk universitas chart):",
appsJson?.message
);
}
const stats = statsJson.data;
const positions = positionJson.data || [];
const countries = countryJson.data || [];
// === Stat Cards === // === Stat Cards ===
const totalPendaftar = rows.length;
const totalDiterima = rows.filter((r) => norm(r.status) === "diterima").length;
const totalDitolak = rows.filter((r) => norm(r.status) === "ditolak").length;
const sedangDiproses = rows.filter((r) => norm(r.status) === "diproses").length;
const statCardsFromApi: StatCard[] = [ const statCardsFromApi: StatCard[] = [
{ title: "Total Pendaftar", value: totalPendaftar, icon: <Users size={24} />, iconColor: "#0088ff" }, {
{ title: "Total Diterima", value: totalDiterima, icon: <UserCheck size={24} />, iconColor: "#00cc00" }, title: "Total Pendaftar",
{ title: "Total Ditolak", value: totalDitolak, icon: <UserX size={24} />, iconColor: "#ff3333" }, value: stats.total_pendaftar,
{ title: "Sedang Diproses", value: sedangDiproses, icon: <Clock size={24} />, iconColor: "#ffaa00" }, icon: <Users size={24} />,
iconColor: "#0088ff",
},
{
title: "Total Diterima",
value: stats.total_diterima,
icon: <UserCheck size={24} />,
iconColor: "#00cc00",
},
{
title: "Total Ditolak",
value: stats.total_ditolak,
icon: <UserX size={24} />,
iconColor: "#ff3333",
},
{
title: "Sedang Diproses",
value: stats.sedang_diproses,
icon: <Clock size={24} />,
iconColor: "#ffaa00",
},
]; ];
// === Position (dari nested lowongan_magang.posisi) === // === Position ===
const positionCounts = groupCount( const positionCounts: ChartDataItem[] = positions.map((p) => ({
rows.map((r) => (r.lowongan_magang?.posisi ?? "").trim()) name: p.posisi,
value: p.total,
}));
// === Universitas ===
const applicants = appsJson?.status ? appsJson.data || [] : [];
const uniCountsRaw = groupCount(
applicants.map((a) => (a.universitas ?? "").toString())
); );
// === Penerimaan === // Top 5 + Lainnya
// hanya isi kalau ada data, biar fallback EMPTY_PIE aktif saat kosong const topN = 5;
const penerimaanCounts: ChartDataItem[] = totalPendaftar > 0 const top = uniCountsRaw.slice(0, topN);
? [ const rest = uniCountsRaw.slice(topN);
{ name: "Total Pendaftar", value: totalPendaftar }, const othersValue = rest.reduce((acc, x) => acc + x.value, 0);
{ name: "Total Diterima", value: totalDiterima }, const uniCounts: ChartDataItem[] =
{ name: "Total Ditolak", value: totalDitolak }, othersValue > 0 ? [...top, { name: "Lainnya", value: othersValue }] : top;
]
: []; // === Negara ===
// === Negara (dari /statistics/country?batch=batchNumber) ===
const countries = countryRes.data?.data ?? [];
const negaraCounts: ChartDataItem[] = countries.map((c) => ({ const negaraCounts: ChartDataItem[] = countries.map((c) => ({
name: c.negara, name: c.negara,
value: c.total, value: c.total,
})); }));
// === Universitas (dari /statistics/university?batch=batchNumber) === // === Penerimaan ===
const uniRaw: Array<{ universitas: string; total: number }> = const penerimaanCounts: ChartDataItem[] = [
(uniRes.data as any)?.data ?? []; { name: "Total Pendaftar", value: stats.total_pendaftar },
const uniSorted = [...uniRaw].sort((a, b) => b.total - a.total); { name: "Total Diterima", value: stats.total_diterima },
const top = uniSorted.slice(0, 5); { name: "Total Ditolak", value: stats.total_ditolak },
const othersValue = uniSorted.slice(5).reduce((acc, x) => acc + x.total, 0);
const uniCounts: ChartDataItem[] = [
...top.map((u) => ({ name: u.universitas, value: u.total })),
...(othersValue > 0 ? [{ name: "Lainnya", value: othersValue }] : []),
]; ];
setStatCards(statCardsFromApi); setStatCards(statCardsFromApi);
setPositionData(positionCounts); setPositionData(positionCounts);
setPenerimaanData(penerimaanCounts);
setNegaraData(negaraCounts);
setUniversitasData(uniCounts); setUniversitasData(uniCounts);
setNegaraData(negaraCounts);
setPenerimaanData(penerimaanCounts);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {
console.error("Error fetching dashboard:", err); console.error("Terjadi kesalahan saat mengambil data dashboard:", err);
setErrorMessage(getAxiosErrorMessage(err)); setErrorMessage(getAxiosErrorMessage(err));
setLoading(false); setLoading(false);
} }
}; };
fetchDashboardData(); fetchDashboardData();
}, [api, selectedBatchId, batches]); }, [api, selectedBatchId]);
return ( return (
<div className="min-h-screen w-full overflow-hidden"> <div className="min-h-screen w-full overflow-hidden">
@@ -377,11 +393,13 @@ const Dashboard: React.FC = () => {
<h2 className="text-[24px] font-semibold">Dashboard</h2> <h2 className="text-[24px] font-semibold">Dashboard</h2>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* Batch Filter */}
{/* Batch Filter */}
<div className="relative"> <div className="relative">
{/* Batch Filter */}
<button <button
onClick={() => setBatchDropdownOpen((o) => !o)} onClick={() => setBatchDropdownOpen((o) => !o)}
className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px] text-gray-700 hover:bg-gray-50"> className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px] text-gray-700 hover:bg-gray-50"
>
{selectedBatch {selectedBatch
? `Batch ${selectedBatch.batch_number} - ${new Date(selectedBatch.created_at).getFullYear()}` ? `Batch ${selectedBatch.batch_number} - ${new Date(selectedBatch.created_at).getFullYear()}`
: "Pilih Batch"} : "Pilih Batch"}
@@ -398,8 +416,8 @@ const Dashboard: React.FC = () => {
setBatchDropdownOpen(false); setBatchDropdownOpen(false);
}} }}
className={`flex w-full items-center justify-between px-4 py-2 text-left text-sm hover:bg-gray-50 ${b.id === selectedBatchId className={`flex w-full items-center justify-between px-4 py-2 text-left text-sm hover:bg-gray-50 ${b.id === selectedBatchId
? "font-semibold text-blue-600" ? "font-semibold text-blue-600"
: "text-gray-700" : "text-gray-700"
}`} }`}
> >
<span> <span>
@@ -414,16 +432,16 @@ const Dashboard: React.FC = () => {
))} ))}
</div> </div>
)} )}
</div>
{/* Search Box */} {/* Search Box */}
{/* <div className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px]"> <div className="flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-[20px] py-[10px]">
<Search size={18} className="text-gray-400" /> <Search size={18} className="text-gray-400" />
<input <input
type="text" type="text"
placeholder="Nama, Jurusan," placeholder="Nama, Jurusan,"
className="w-52 text-sm text-gray-700 placeholder-gray-400 outline-none" className="w-52 text-sm text-gray-700 placeholder-gray-400 outline-none"
/> />
</div> */}
</div> </div>
</div> </div>
</div> </div>
@@ -477,7 +495,7 @@ const Dashboard: React.FC = () => {
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<PieChart> <PieChart>
<Pie <Pie
data={positionData.length > 0 ? positionData : EMPTY_PIE} data={positionData}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={0} innerRadius={0}
@@ -485,13 +503,13 @@ const Dashboard: React.FC = () => {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{(positionData.length > 0 ? positionData : EMPTY_PIE).map((_, index) => ( {positionData.map((_, index) => (
<Cell <Cell
key={`position-cell-${index}`} key={`position-cell-${index}`}
fill={ fill={
positionData.length > 0 CATEGORY_COLORS[
? CATEGORY_COLORS[index % CATEGORY_COLORS.length] index % CATEGORY_COLORS.length
: EMPTY_COLOR[0] ]
} }
/> />
))} ))}
@@ -513,7 +531,7 @@ const Dashboard: React.FC = () => {
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<PieChart> <PieChart>
<Pie <Pie
data={universitasData.length > 0 ? universitasData : EMPTY_PIE} data={universitasData}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={0} innerRadius={0}
@@ -521,13 +539,13 @@ const Dashboard: React.FC = () => {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{(universitasData.length > 0 ? universitasData : EMPTY_PIE).map((_, index) => ( {universitasData.map((_, index) => (
<Cell <Cell
key={`universitas-cell-${index}`} key={`universitas-cell-${index}`}
fill={ fill={
universitasData.length > 0 CATEGORY_COLORS[
? CATEGORY_COLORS[index % CATEGORY_COLORS.length] index % CATEGORY_COLORS.length
: EMPTY_COLOR[0] ]
} }
/> />
))} ))}
@@ -552,7 +570,7 @@ const Dashboard: React.FC = () => {
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<PieChart> <PieChart>
<Pie <Pie
data={negaraData.length > 0 ? negaraData : EMPTY_PIE} data={negaraData}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={0} innerRadius={0}
@@ -560,14 +578,10 @@ const Dashboard: React.FC = () => {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{(negaraData.length > 0 ? negaraData : EMPTY_PIE).map((_, index) => ( {negaraData.map((_, index) => (
<Cell <Cell
key={`negara-cell-${index}`} key={`negara-cell-${index}`}
fill={ fill={CATEGORY_COLORS[index % CATEGORY_COLORS.length]}
negaraData.length > 0
? CATEGORY_COLORS[index % CATEGORY_COLORS.length]
: EMPTY_COLOR[0]
}
/> />
))} ))}
</Pie> </Pie>
@@ -588,7 +602,7 @@ const Dashboard: React.FC = () => {
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<PieChart> <PieChart>
<Pie <Pie
data={penerimaanData.length > 0 ? penerimaanData : EMPTY_PIE} data={penerimaanData}
cx="50%" cx="50%"
cy="50%" cy="50%"
innerRadius={0} innerRadius={0}
@@ -596,13 +610,13 @@ const Dashboard: React.FC = () => {
paddingAngle={2} paddingAngle={2}
dataKey="value" dataKey="value"
> >
{(penerimaanData.length > 0 ? penerimaanData : EMPTY_PIE).map((_, index) => ( {penerimaanData.map((_, index) => (
<Cell <Cell
key={`penerimaan-cell-${index}`} key={`penerimaan-cell-${index}`}
fill={ fill={
penerimaanData.length > 0 CATEGORY_COLORS[
? CATEGORY_COLORS[index % CATEGORY_COLORS.length] index % CATEGORY_COLORS.length
: EMPTY_COLOR[0] ]
} }
/> />
))} ))}
@@ -622,4 +636,4 @@ const Dashboard: React.FC = () => {
); );
}; };
export default Dashboard; export default Dashboard;
+2 -2
View File
@@ -74,7 +74,7 @@ export default function InternifyLogin() {
</span> </span>
<input <input
type="email" type="email"
placeholder="name@company.com" placeholder="Email Account"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors" className="w-full pl-10 pr-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors"
@@ -90,7 +90,7 @@ export default function InternifyLogin() {
</span> </span>
<input <input
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
placeholder="••••••••" placeholder="Account Password"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors" className="w-full pl-10 pr-10 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 focus:outline-none focus:border-red-500 focus:ring-1 focus:ring-red-500 transition-colors"
+10 -7
View File
@@ -3,7 +3,7 @@ import { useCurrentUser, useUpdateProfile } from "../../hooks/useUser";
import { customToast } from "../utils/showToast"; import { customToast } from "../utils/showToast";
import { Pen } from "lucide-react"; import { Pen } from "lucide-react";
export default function MentorSettingsPage() { export default function SettingsPage() {
const { user, loading: userLoading } = useCurrentUser(); const { user, loading: userLoading } = useCurrentUser();
const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile(); const { save, loading: saving, error: errorMsg, successMsg } = useUpdateProfile();
const isIntern = user?.role === "intern"; const isIntern = user?.role === "intern";
@@ -23,9 +23,7 @@ export default function MentorSettingsPage() {
if (user) { if (user) {
setFormData({ setFormData({
fullName: fullName:
user.full_name || user.full_name || "",
`${user.nama_depan ?? ""} ${user.nama_belakang ?? ""}`.trim() ||
"",
email: user.email || "", email: user.email || "",
bio: user.professional_bio || "", bio: user.professional_bio || "",
}); });
@@ -121,7 +119,7 @@ export default function MentorSettingsPage() {
<div className="flex items-center gap-5 py-2"> <div className="flex items-center gap-5 py-2">
{/* Avatar dengan badge edit */} {/* Avatar dengan badge edit */}
<div className="relative flex-shrink-0"> <div className="relative flex-shrink-0">
<div className="w-20 h-20 rounded-xl border-2 border-red-100 overflow-hidden bg-gray-100 flex items-center justify-center shadow-sm"> <div className="w-16 h-16 rounded-xl border-2 border-red-100 overflow-hidden bg-gray-100 flex items-center justify-center shadow-sm">
{photoPreview && !imageError ? ( {photoPreview && !imageError ? (
<img <img
src={photoPreview} src={photoPreview}
@@ -147,7 +145,7 @@ export default function MentorSettingsPage() {
</div> </div>
{/* Info & Action Buttons */} {/* Info & Action Buttons */}
{!isIntern && {!isIntern ? (
<div> <div>
<h3 className="text-sm font-bold text-gray-900 mb-0.5">Profile Photo</h3> <h3 className="text-sm font-bold text-gray-900 mb-0.5">Profile Photo</h3>
<p className="text-xs text-gray-400 mb-3"> <p className="text-xs text-gray-400 mb-3">
@@ -175,7 +173,12 @@ export default function MentorSettingsPage() {
</button> </button>
</div> </div>
</div> </div>
} ) : (
<div>
<h3 className="text-sm font-bold text-gray-900 mb-0.5">Nama</h3>
<p className="text-base text-gray-700 font-medium">{user?.full_name || formData.fullName || 'User'}</p>
</div>
)}
{/* Hidden file input */} {/* Hidden file input */}
<input <input
+2 -4
View File
@@ -12,19 +12,17 @@ export default function ForumTab({ project }: { project: Project }) {
<HeroProject title={project.project_name} description={project.description} /> <HeroProject title={project.project_name} description={project.description} />
{/* Loading skeleton */} {/* Loading skeleton */}
{/* {loading && ( {loading && (
<div className="flex flex-col gap-3 mt-5"> <div className="flex flex-col gap-3 mt-5">
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
<div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" /> <div key={i} className="h-24 bg-gray-100 rounded-xl animate-pulse" />
))} ))}
</div> </div>
)} */} )}
{/* Error */} {/* Error */}
{error && <p className="text-red-500 text-sm mt-5">{error}</p>} {error && <p className="text-red-500 text-sm mt-5">{error}</p>}
{/* Timeline + Task list — hanya tampil jika ada task */}
{/* {!loading && tasks.length === 0 ? ( */}
{tasks.length === 0 ? ( {tasks.length === 0 ? (
<p className="text-md text-font-shade mt-5">Belum ada tugas yang dapat dikerjakan.</p> <p className="text-md text-font-shade mt-5">Belum ada tugas yang dapat dikerjakan.</p>
) : ( ) : (
@@ -3,12 +3,16 @@ import { FileText, Info } from "lucide-react";
import TaskFormFile from "./components/TaskForm-file"; import TaskFormFile from "./components/TaskForm-file";
import TaskFormLink from "./components/TaskForm-link"; import TaskFormLink from "./components/TaskForm-link";
import { useTaskDetail } from "../../../hooks/useTasks"; import { useTaskDetail } from "../../../hooks/useTasks";
import { useProjectDetail } from "../../../hooks/useProjects";
export default function TaskSubmission() { export default function TaskSubmission() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const submissionType = (searchParams.get("type") ?? "file") as "file_upload" | "url_link"; const submissionType = (searchParams.get("type") ?? "file") as "file_upload" | "url_link";
const { task, loading, error } = useTaskDetail(taskSlug!, slug); const { task, loading, error } = useTaskDetail(taskSlug!, slug);
const { project } = useProjectDetail(slug!);
const isProjectActive = project?.status === "active";
const formatDeadline = (dateStr: string) => const formatDeadline = (dateStr: string) =>
new Date(dateStr).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }); new Date(dateStr).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" });
@@ -53,6 +57,7 @@ export default function TaskSubmission() {
projectId={String(task.project.id)} projectId={String(task.project.id)}
deadline={new Date(task.deadline_at)} deadline={new Date(task.deadline_at)}
initialSubmission={task.my_submission} initialSubmission={task.my_submission}
isProjectActive={isProjectActive}
/> />
) : ( ) : (
<TaskFormLink <TaskFormLink
@@ -60,6 +65,7 @@ export default function TaskSubmission() {
projectId={String(task.project.id)} projectId={String(task.project.id)}
deadline={new Date(task.deadline_at)} deadline={new Date(task.deadline_at)}
initialSubmission={task.my_submission} initialSubmission={task.my_submission}
isProjectActive={isProjectActive}
/> />
)} )}
@@ -7,7 +7,7 @@ const HomeHeader = () => {
useEffect(() => { useEffect(() => {
api.get("/auth-api/me") api.get("/auth-api/me")
.then((res) => { .then((res) => {
setName(res.data.data.full_name || res.data.data.nama_depan); setName(res.data.data.full_name || (res.data.data.nama_depan + " " + res.data.data.nama_belakang));
}) })
.catch((err) => { .catch((err) => {
console.error("Failed to fetch user:", err); console.error("Failed to fetch user:", err);
@@ -8,7 +8,7 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] })
today.setHours(0, 0, 0, 0); today.setHours(0, 0, 0, 0);
return ( return (
<div className=""> <div className="">
<div className="flex items-center gap-1.5 mb-4 justify-between"> <div className="flex items-center gap-1.5 mb-1 md:mb-4 justify-between">
<span className="font-semibold text-md text-font-shade">Timeline</span> <span className="font-semibold text-md text-font-shade">Timeline</span>
</div> </div>
<ul className="relative pl-4"> <ul className="relative pl-4">
@@ -20,7 +20,6 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] })
const active = isToday || isPast; const active = isToday || isPast;
return ( return (
<li key={task.id} className="relative mb-6 list-none"> <li key={task.id} className="relative mb-6 list-none">
{/* Vertical line ke bawah, kecuali item terakhir */}
{index < tasks.length - 1 && ( {index < tasks.length - 1 && (
<div className="absolute -left-[1.0625rem] top-3 w-px h-full bg-[#ddd]" /> <div className="absolute -left-[1.0625rem] top-3 w-px h-full bg-[#ddd]" />
)} )}
@@ -30,10 +29,10 @@ export default function ProjectTimeline({ tasks }: { tasks: InternTaskItem[] })
active ? 'bg-red' : 'bg-gray-400' active ? 'bg-red' : 'bg-gray-400'
} ${isToday ? 'shadow-md' : ''}`} } ${isToday ? 'shadow-md' : ''}`}
/> />
<div className={`text-[11px] font-semibold ${isToday ? 'text-[#C0392B]' : 'text-[#888]'}`}> <div className={`text-[11px] font-semibold ${isToday ? 'text-red' : 'text-font'}`}>
{isToday ? 'Hari ini' : formatDate(task.deadline_at)} {isToday ? 'Hari ini' : formatDate(task.deadline_at)}
</div> </div>
<div className="text-xs text-[#555] mt-px leading-[1.3]"> <div className="text-xs text-font mt-px leading-[1.3]">
{task.title} {task.title}
</div> </div>
</li> </li>
@@ -10,6 +10,7 @@ interface SubmitStatusTableProps {
deadline?: Date; deadline?: Date;
onEdit: () => void; onEdit: () => void;
onDelete: () => void; onDelete: () => void;
disabled: boolean;
} }
function getTimeEarly(submittedAt: Date, deadline: Date): string { function getTimeEarly(submittedAt: Date, deadline: Date): string {
@@ -32,6 +33,7 @@ export default function SubmitStatusTable({
deadline, deadline,
onEdit, onEdit,
onDelete, onDelete,
disabled = false,
}: SubmitStatusTableProps) { }: SubmitStatusTableProps) {
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
@@ -86,13 +88,15 @@ export default function SubmitStatusTable({
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={onEdit} onClick={onEdit}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors" disabled={disabled}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
> >
Edit Submission Edit Submission
</button> </button>
<button <button
onClick={() => setConfirmDelete(true)} onClick={() => setConfirmDelete(true)}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:text-red-700 hover:bg-red-50 hover:border-red-200 transition-colors" disabled={disabled}
className="text-xs font-medium px-4 py-1.5 rounded-lg bg-white border border-gray-300 text-gray-700 hover:text-red-700 hover:bg-red-50 hover:border-red-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white"
> >
Delete Submission Delete Submission
</button> </button>
@@ -11,9 +11,10 @@ interface TaskFormFileProps {
projectId?: string; projectId?: string;
deadline?: Date; deadline?: Date;
initialSubmission?: TaskSubmissionData | null; initialSubmission?: TaskSubmissionData | null;
isProjectActive?: boolean;
} }
export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) { export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission, isProjectActive = true }: TaskFormFileProps) {
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId); const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId);
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [isDragActive, setIsDragActive] = useState(false); const [isDragActive, setIsDragActive] = useState(false);
@@ -119,6 +120,7 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
deadline={deadline} deadline={deadline}
onEdit={() => setIsEditing(true)} onEdit={() => setIsEditing(true)}
onDelete={handleDelete} onDelete={handleDelete}
disabled={!isProjectActive}
/> />
); );
} }
@@ -188,8 +190,8 @@ export default function TaskFormFile({ taskId, projectId, deadline, initialSubmi
</button> </button>
)} )}
<button <button
type="submit" disabled={loading} type="submit" disabled={loading || !isProjectActive}
className="w-full bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white font-bold py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer" className="w-full bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white font-bold py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer disabled:cursor-not-allowed disabled:hover:bg-[#990000]"
> >
{loading ? "Mengirim..." : isEditing ? "Update Submission" : "Save Changes"} {loading ? "Mengirim..." : isEditing ? "Update Submission" : "Save Changes"}
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" /> <LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
@@ -11,13 +11,15 @@ interface TaskFormLinkProps {
projectId?: string; projectId?: string;
deadline?: Date; deadline?: Date;
initialSubmission?: TaskSubmissionData | null; initialSubmission?: TaskSubmissionData | null;
isProjectActive?: boolean;
} }
export default function TaskFormLink({ export default function TaskFormLink({
taskId, taskId,
projectId, projectId,
deadline, deadline,
initialSubmission initialSubmission,
isProjectActive = true,
}: TaskFormLinkProps) { }: TaskFormLinkProps) {
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId); const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId);
const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? ""); const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? "");
@@ -88,6 +90,7 @@ export default function TaskFormLink({
deadline={deadline} deadline={deadline}
onEdit={() => setIsEditing(true)} onEdit={() => setIsEditing(true)}
onDelete={handleDelete} onDelete={handleDelete}
disabled={!isProjectActive}
/> />
); );
} }
@@ -115,8 +118,8 @@ export default function TaskFormLink({
</p> </p>
</div> </div>
<button <button
type="submit" disabled={loading} type="submit" disabled={loading || !isProjectActive}
className="w-full bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white font-bold py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer" className="w-full bg-[#B30000] hover:bg-[#990000] disabled:opacity-60 text-white font-bold py-3 rounded-xl transition-colors shadow-md flex items-center justify-center gap-2 tracking-wide cursor-pointer disabled:cursor-not-allowed disabled:hover:bg-[#990000]"
> >
{loading ? "Mengirim..." : isEditing ? "Update Submission" : "Submit Task"} {loading ? "Mengirim..." : isEditing ? "Update Submission" : "Submit Task"}
<LuSendHorizontal className="w-4 h-4 stroke-[2.5]" /> <LuSendHorizontal className="w-4 h-4 stroke-[2.5]" />
@@ -6,7 +6,7 @@ import { useProjectsByRole } from '../../../hooks/useListProjects';
export default function MentorProjectsPage() { export default function MentorProjectsPage() {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { projects, loading, error, refetch } = useProjectsByRole(); const { projects, loading, error, refetch } = useProjectsByRole("active");
const openDialog = () => { const openDialog = () => {
setIsModalOpen(true); setIsModalOpen(true);
@@ -49,12 +49,12 @@ export default function MentorProjectsPage() {
<div className="flex flex-wrap gap-4 items-stretch"> <div className="flex flex-wrap gap-4 items-stretch">
{projects.map(project => ( {projects.map(project => (
<MentorProjectCard <MentorProjectCard
key={project.id} key={project.id}
{...project} {...project}
onArchived={refetch} onArchived={refetch}
onCompleted={refetch} onCompleted={refetch}
/> />
))} ))}
</div> </div>
@@ -4,6 +4,7 @@ import { Calendar, Edit } from "lucide-react";
import EditSubmissionModal from "./components/EditSubmissionTask"; import EditSubmissionModal from "./components/EditSubmissionTask";
import { useTaskSubmissions } from "../../../hooks/useTasks"; import { useTaskSubmissions } from "../../../hooks/useTasks";
import type { ProjectTask } from "../../../types/task.types"; import type { ProjectTask } from "../../../types/task.types";
import { useProjectDetail } from "../../../hooks/useProjects";
const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-'); const toSlug = (name: string) => name.toLowerCase().replace(/\s+/g, '-');
@@ -12,14 +13,13 @@ export default function MentorProjectsDetailPage() {
const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>(); const { slug, taskSlug } = useParams<{ slug: string; taskSlug: string }>();
const { task, loading, error, refetch } = useTaskSubmissions(taskSlug!, slug); const { task, loading, error, refetch } = useTaskSubmissions(taskSlug!, slug);
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const { project } = useProjectDetail(slug!);
const isProjectActive = project?.status === "active"
// Di MentorProjectsDetailPage
const handleEditSuccess = (updatedTask: ProjectTask) => { const handleEditSuccess = (updatedTask: ProjectTask) => {
if (updatedTask.slug !== taskSlug) { if (updatedTask.slug !== taskSlug) {
// Slug berubah, navigate ke URL baru
navigate(`/mentor/projects/${slug}/${updatedTask.slug}`, { replace: true }); navigate(`/mentor/projects/${slug}/${updatedTask.slug}`, { replace: true });
} else { } else {
// Slug sama, cukup refetch
refetch(); refetch();
} }
}; };
@@ -50,8 +50,9 @@ export default function MentorProjectsDetailPage() {
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-[#1a1a1a]">{task.title}</h1> <h1 className="text-3xl font-bold text-[#1a1a1a]">{task.title}</h1>
<button <button
className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all" className="flex items-center gap-2 border border-gray-400 text-gray-700 px-4 py-2 rounded-lg text-sm font-medium hover:bg-gray-50 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setOpenModal(true)} onClick={() => setOpenModal(true)}
disabled={!isProjectActive}
> >
<Edit size={16} /> <Edit size={16} />
Edit Task Edit Task
@@ -5,17 +5,13 @@ import { useProjectTasks } from '../../../hooks/useTasks';
import type { Project } from '../../../types/project.types'; import type { Project } from '../../../types/project.types';
export default function MentorForumTab({ project }: { project: Project }) { export default function MentorForumTab({ project }: { project: Project }) {
// Gunakan project.id dari props — route pakai :slug, bukan :id
const { tasks, loading, error } = useProjectTasks(project.id); const { tasks, loading, error } = useProjectTasks(project.id);
if (error) return <div>Error: {error}</div>; if (error) return <div>Error: {error}</div>;
return ( return (
<div> <div>
{/* Hero banner */}
<HeroProject title={project.project_name} description={project.description} /> <HeroProject title={project.project_name} description={project.description} />
{/* Loading skeleton */}
{loading && ( {loading && (
<div className="flex flex-col gap-3 mt-5"> <div className="flex flex-col gap-3 mt-5">
{[...Array(3)].map((_, i) => ( {[...Array(3)].map((_, i) => (
@@ -24,18 +20,15 @@ export default function MentorForumTab({ project }: { project: Project }) {
</div> </div>
)} )}
{/* Timeline + Task list — hanya tampil jika ada task */}
{!loading && tasks.length === 0 ? ( {!loading && tasks.length === 0 ? (
<p className="text-md text-font-shade mt-5">Belum ada tugas yang dikerjakan.</p> <p className="text-md text-font-shade mt-5">Belum ada tugas yang dikerjakan.</p>
) : ( ) : (
!loading && ( !loading && (
<div className="flex flex-col gap-7 md:flex-row md:items-start mt-5"> <div className="flex flex-col gap-7 md:flex-row md:items-start mt-5">
{/* Kolom kiri: Timeline */}
<div className="w-full md:w-1/5 bg-white rounded-xl p-5 border border-box-border shrink-0"> <div className="w-full md:w-1/5 bg-white rounded-xl p-5 border border-box-border shrink-0">
<ProjectTimeline tasks={tasks} /> <ProjectTimeline tasks={tasks} />
</div> </div>
{/* Kolom kanan: Task cards */}
<div className="w-full border-t border-box-border md:border-t-0 pt-4 md:pt-0 md:w-4/5"> <div className="w-full border-t border-box-border md:border-t-0 pt-4 md:pt-0 md:w-4/5">
{tasks.map((task) => ( {tasks.map((task) => (
<MentorTaskCard key={task.id} task={task} /> <MentorTaskCard key={task.id} task={task} />
@@ -9,6 +9,7 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [members, setMembers] = useState<ProjectMember[]>(project.members); const [members, setMembers] = useState<ProjectMember[]>(project.members);
const { remove, loading } = useRemoveMember(); const { remove, loading } = useRemoveMember();
const isProjectActive = project?.status === "active"
const handleModalClose = (finalMembers: ProjectMember[]) => { const handleModalClose = (finalMembers: ProjectMember[]) => {
setMembers(finalMembers); setMembers(finalMembers);
@@ -58,7 +59,9 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
</h3> </h3>
<button <button
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
className='bg-gray-300 border border-gray-500 px-2 py-1 rounded-md hover:bg-gray-400 transition-colors' disabled={!isProjectActive}
className={`border border-gray-500 px-2 py-1 rounded-md transition-colors ${!isProjectActive ? 'bg-gray-100 text-gray-400 cursor-not-allowed' : 'bg-gray-300 hover:bg-gray-400'
}`}
> >
<span className='flex items-center gap-3 text-[11px]'> <span className='flex items-center gap-3 text-[11px]'>
<Plus className='w-3 h-3'/>Add <Plus className='w-3 h-3'/>Add
@@ -82,8 +85,8 @@ export default function InternsTab({ project }: { project: ProjectDetail }) {
<div> <div>
<button <button
onClick={() => handleRemove(member.id)} onClick={() => handleRemove(member.id)}
disabled={loading} disabled={loading || !isProjectActive}
className='flex items-center gap-1 text-[11px] bg-red-600 text-white py-1 px-2 rounded-md disabled:opacity-50 hover:bg-red-700 transition-colors' className='flex items-center gap-1 text-[11px] bg-red-600 text-white py-1 px-2 rounded-md disabled:opacity-50 hover:bg-red-700 transition-colors disabled:cursor-not-allowed'
> >
<Plus className='w-4 h-4'/>Delete <Plus className='w-4 h-4'/>Delete
</button> </button>
+13 -7
View File
@@ -1,6 +1,5 @@
import { useProjectTasks, useDeleteTask } from '../../../hooks/useTasks'; import { useProjectTasks, useDeleteTask } from '../../../hooks/useTasks';
import type { ProjectDetail } from '../../../types/project.types'; import type { InternTaskItem, ProjectDetail } from '../../../types/project.types';
import type { ProjectTask } from '../../../types/task.types';
import { ClipboardList, Pencil, Trash2 } from 'lucide-react'; import { ClipboardList, Pencil, Trash2 } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import CreateTaskModal from './components/CreateTaskDialog'; import CreateTaskModal from './components/CreateTaskDialog';
@@ -12,9 +11,13 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
const { remove } = useDeleteTask(); const { remove } = useDeleteTask();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false);
const [selectedTask, setSelectedTask] = useState<ProjectTask | null>(null); const [selectedTask, setSelectedTask] = useState<InternTaskItem | null>(null);
const isProjectActive = project?.status === "active"
const handleDeleteTask = async (taskId: number, taskTitle: string) => {
const confirmed = window.confirm(`Yakin ingin menghapus task “${taskTitle}”?`);
if (!confirmed) return;
const handleDeleteTask = async (taskId: number,taskTitle: string) => {
const del = await remove(taskId, String(project.id)); const del = await remove(taskId, String(project.id));
if (del) { if (del) {
try { try {
@@ -42,7 +45,8 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
<h2 className="text-3xl font-bold text-slate-800 tracking-tight">Tasks</h2> <h2 className="text-3xl font-bold text-slate-800 tracking-tight">Tasks</h2>
<button <button
onClick={() => setIsModalOpen(true)} onClick={() => setIsModalOpen(true)}
className="bg-[#C0392B] hover:bg-[#A93226] text-white px-6 py-2.5 rounded-full font-bold text-sm transition-colors shadow-sm" disabled={!isProjectActive}
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
Add Task Add Task
</button> </button>
@@ -65,13 +69,15 @@ export default function TaskTab({ project }: { project: ProjectDetail }) {
<div className="flex items-center gap-4 text-slate-600 shrink-0"> <div className="flex items-center gap-4 text-slate-600 shrink-0">
<button <button
onClick={() => { setSelectedTask(task); setEditModalOpen(true); }} onClick={() => { setSelectedTask(task); setEditModalOpen(true); }}
className="hover:text-blue-600 transition-colors p-1" disabled={!isProjectActive}
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Pencil size={20} /> <Pencil size={20} />
</button> </button>
<button <button
onClick={() => handleDeleteTask(task.id, task.title)} onClick={() => handleDeleteTask(task.id, task.title)}
className="hover:text-red-600 transition-colors p-1" disabled={!isProjectActive}
className="border border-gray-500 text-font-shade rounded-lg px-2 py-1 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Trash2 size={20} /> <Trash2 size={20} />
</button> </button>
@@ -42,7 +42,7 @@ export default function ViewInternSubmission() {
); );
return ( return (
<div className="space-y-4"> <div className="pt-8 space-y-4">
{/* Header */} {/* Header */}
<div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6"> <div className="w-full bg-box-secondary rounded-2xl border border-box-border p-6">
<div className="space-y-1"> <div className="space-y-1">
@@ -47,8 +47,8 @@ export default function ArchiveProjectDialog({
{/* Header */} {/* Header */}
<div className="dialog-header"> <div className="dialog-header">
<div className="flex items-center gap-2.5 px-3"> <div className="flex items-center gap-2.5 px-3">
<div className="w-9 h-9 rounded-full bg-yellow-100 flex items-center justify-center shrink-0"> <div className="w-9 h-9 rounded-full bg-red-100 flex items-center justify-center shrink-0">
<Archive className="w-4 h-4 text-yellow-600" /> <Archive className="w-4 h-4 text-red-600" />
</div> </div>
<div> <div>
<h2 className="text-base font-bold text-gray-900 tracking-tight"> <h2 className="text-base font-bold text-gray-900 tracking-tight">
@@ -74,8 +74,8 @@ export default function ArchiveProjectDialog({
Apakah kamu yakin ingin mengarsipkan project{" "} Apakah kamu yakin ingin mengarsipkan project{" "}
<span className="font-semibold text-gray-900">"{projectName}"</span>? <span className="font-semibold text-gray-900">"{projectName}"</span>?
</p> </p>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg px-4 py-3"> <div className="bg-red-50 border border-red-200 rounded-lg px-4 py-3">
<p className="text-xs text-yellow-700 leading-relaxed"> <p className="text-xs text-red-700 leading-relaxed">
<span className="font-bold">Perhatian:</span> Project yang diarsipkan <span className="font-bold">Perhatian:</span> Project yang diarsipkan
tidak akan bisa diakses oleh intern dan tidak muncul di daftar aktif. tidak akan bisa diakses oleh intern dan tidak muncul di daftar aktif.
Kamu masih bisa melihatnya di tab Archived. Kamu masih bisa melihatnya di tab Archived.
@@ -96,7 +96,7 @@ export default function ArchiveProjectDialog({
type="button" type="button"
onClick={onConfirm} onClick={onConfirm}
disabled={loading} disabled={loading}
className="w-full sm:flex-1 py-2.5 bg-yellow-500 hover:bg-yellow-600 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2" className="w-full sm:flex-1 py-2.5 bg-red-500 hover:bg-red-600 disabled:opacity-60 text-white text-sm font-bold rounded-xl transition-colors shadow-md flex items-center justify-center gap-2"
> >
<Archive className="w-4 h-4" /> <Archive className="w-4 h-4" />
{loading ? "Mengarsipkan..." : "Ya, Arsipkan"} {loading ? "Mengarsipkan..." : "Ya, Arsipkan"}
@@ -27,18 +27,6 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const emailSectionRef = useRef<HTMLDivElement>(null); const emailSectionRef = useRef<HTMLDivElement>(null);
// Tutup dropdown saat klik di luar area input
const handleClickOutside = useCallback((e: MouseEvent) => {
if (emailSectionRef.current && !emailSectionRef.current.contains(e.target as Node)) {
setShowDropdown(false);
}
}, []);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [handleClickOutside]);
const projectIcons = PROJECT_ICON_CODES.map((id) => ({ const projectIcons = PROJECT_ICON_CODES.map((id) => ({
id, id,
icon: PROJECT_ICON_MAP[id], icon: PROJECT_ICON_MAP[id],
@@ -77,11 +65,6 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
} }
}; };
// const handleAddEmailFromForm = (e: React.FormEvent) => {
// e.preventDefault();
// handleAddEmail();
// };
const handleRemoveEmail = (emailToRemove: string) => { const handleRemoveEmail = (emailToRemove: string) => {
setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove)); setInvitedEmails(invitedEmails.filter(email => email !== emailToRemove));
}; };
@@ -171,7 +154,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
placeholder="e.g., Q3 Infrastructure Upgrade" placeholder="e.g., Q3 Infrastructure Upgrade"
value={projectName} value={projectName}
onChange={(e) => setProjectName(e.target.value)} onChange={(e) => setProjectName(e.target.value)}
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" className="w-full px-4 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
/> />
</div> </div>
@@ -185,7 +168,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
placeholder="Outline the project goals, deliverables, and technical requirements..." placeholder="Outline the project goals, deliverables, and technical requirements..."
value={projectDescription} value={projectDescription}
onChange={(e) => setProjectDescription(e.target.value)} onChange={(e) => setProjectDescription(e.target.value)}
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed" className="w-full px-4 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium resize-none leading-relaxed"
/> />
</div> </div>
@@ -213,12 +196,12 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
} }
if (e.key === 'Escape') setShowDropdown(false); if (e.key === 'Escape') setShowDropdown(false);
}} }}
className="w-full pl-10 pr-20 py-2.5 bg-white border border-gray-200 rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium" className="w-full pl-10 pr-20 py-2.5 bg-white border border-card-outline rounded-lg text-sm placeholder-gray-400 text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium"
/> />
<button <button
type="button" type="button"
onClick={() => handleAddEmail()} onClick={() => handleAddEmail()}
className="absolute right-2 px-3 py-1 bg-gray-100 hover:bg-gray-200 text-gray-800 text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase" className="absolute right-2 px-3 py-1 bg-card hover:bg-card-hover border border-card-outline text-font-shade text-[11px] font-bold rounded-md transition-colors tracking-wider uppercase"
> >
ADD ADD
</button> </button>
@@ -226,7 +209,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
{/* Dropdown daftar intern */} {/* Dropdown daftar intern */}
{showDropdown && ( {showDropdown && (
<div className="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-xl shadow-lg max-h-44 overflow-y-auto"> <div className="absolute z-10 mt-1 w-full bg-white border border-card-outline rounded-xl shadow-lg max-h-44 overflow-y-auto">
{loadingInterns ? ( {loadingInterns ? (
<p className="text-xs text-gray-400 text-center py-3">Memuat data intern...</p> <p className="text-xs text-gray-400 text-center py-3">Memuat data intern...</p>
) : (() => { ) : (() => {
@@ -244,13 +227,10 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
key={intern.id} key={intern.id}
type="button" type="button"
onMouseDown={(e) => { e.preventDefault(); handleAddEmail(intern.email); }} onMouseDown={(e) => { e.preventDefault(); handleAddEmail(intern.email); }}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 transition-colors text-left" className="w-full flex items-center gap-3 px-6 py-2.5 hover:bg-gray-50 transition-colors text-left"
> >
<div className="w-7 h-7 rounded-full bg-red-100 text-red-700 flex items-center justify-center text-[10px] font-bold shrink-0">
{intern.full_name.split(' ').slice(0, 2).map((n) => n[0]).join('').toUpperCase()}
</div>
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<span className="text-xs font-semibold text-gray-800 truncate">{intern.full_name}</span> <span className="text-[13px] font-semibold text-gray-800 truncate">{intern.full_name}</span>
<span className="text-[11px] text-gray-400 truncate">{intern.email}</span> <span className="text-[11px] text-gray-400 truncate">{intern.email}</span>
</div> </div>
</button> </button>
@@ -294,7 +274,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
required required
value={startDate} value={startDate}
onChange={(e) => setStartDate(e.target.value)} onChange={(e) => setStartDate(e.target.value)}
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
/> />
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none"> <span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
</span> </span>
@@ -312,7 +292,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
required required
value={endDate} value={endDate}
onChange={(e) => setEndDate(e.target.value)} onChange={(e) => setEndDate(e.target.value)}
className="w-full px-3 py-2.5 bg-white border border-gray-200 rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none" className="w-full px-3 py-2.5 bg-white border border-card-outline rounded-lg text-sm text-gray-800 focus:outline-none focus:border-red-600 focus:ring-1 focus:ring-red-600 font-medium appearance-none"
/> />
<span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none"> <span className="absolute right-3.5 top-3 text-gray-400 pointer-events-none">
</span> </span>
@@ -329,7 +309,7 @@ export default function CreateProjectModal({ isOpen, onClose, onSuccess }: Creat
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="w-full sm:w-[150px] py-2.5 border border-gray-200 rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors" className="w-full sm:w-[150px] py-2.5 border border-card-outline rounded-xl text-sm font-semibold text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -1,13 +1,14 @@
import { useRef, useEffect, useState } from "react"; import { useRef, useEffect, useState } from "react";
import { X, FileText, Link } from "lucide-react"; import { X, FileText, Link } from "lucide-react";
import { useUpdateTask } from '../../../../hooks/useTasks'; import { useUpdateTask } from '../../../../hooks/useTasks';
import type { ProjectTask, SubmissionType } from '../../../../types/task.types'; import type { SubmissionType } from '../../../../types/task.types';
import { customToast } from '../../../utils/showToast'; import { customToast } from '../../../utils/showToast';
import type { InternTaskItem } from "../../../../types/project.types";
interface EditTaskModalProps { interface EditTaskModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
task: ProjectTask; task: InternTaskItem;
onSuccess: () => void; onSuccess: () => void;
} }
@@ -19,6 +19,8 @@ import Avatar from "../../Avatar";
import ProgressBar from "../../ProgressBar"; import ProgressBar from "../../ProgressBar";
import { customToast } from "../../../utils/showToast"; import { customToast } from "../../../utils/showToast";
import type { Project } from "../../../../types/project.types"; import type { Project } from "../../../../types/project.types";
import { previewCertificate } from "../../../utils/Certificates";
import type { Certificate } from "../../../../types/certificate.types";
interface EligibleInternTableProps { interface EligibleInternTableProps {
interns: InternSubmissionProgress[]; interns: InternSubmissionProgress[];
@@ -72,6 +74,32 @@ export default function EligibleInternTable({
const [generating, setGenerating] = useState(false); const [generating, setGenerating] = useState(false);
const [generateError, setGenerateError] = useState<string | null>(null); const [generateError, setGenerateError] = useState<string | null>(null);
const handlePreview = (intern: InternSubmissionProgress) => {
if (!project?.certificate_template) {
customToast.error("Template belum ada", "Template sertifikat belum diupload.");
return;
}
const mockCert: Certificate = {
id: intern.id_user,
id_project: project.id,
id_user: intern.id_user,
certificate_no: "PREVIEW",
issued_at: new Date().toISOString(),
uuid: "preview-mode",
user: {
id: intern.id_user,
full_name: intern.full_name,
email: intern.email,
},
project: {
id: project.id,
project_name: project.project_name,
description: project.description,
},
};
previewCertificate(mockCert, project.certificate_template);
};
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const totalPages = Math.ceil(interns.length / PER_PAGE); const totalPages = Math.ceil(interns.length / PER_PAGE);
const paginated = interns.slice((page - 1) * PER_PAGE, page * PER_PAGE); const paginated = interns.slice((page - 1) * PER_PAGE, page * PER_PAGE);
@@ -113,7 +141,7 @@ export default function EligibleInternTable({
const templateUrl = resolveImageUrl(project.certificate_template!); const templateUrl = resolveImageUrl(project.certificate_template!);
const duration = formatDateRange(project.start_date, project.end_date); const duration = formatDateRange(project.start_date, project.end_date);
await Promise.all( await Promise.all(
result.map(async (cert) => { result.map(async (cert) => {
const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`; const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`;
@@ -253,7 +281,11 @@ export default function EligibleInternTable({
Nag Intern Nag Intern
</button> </button>
) : ( ) : (
<button className="p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors" title="View"> <button
onClick={() => handlePreview(intern)}
className="p-1.5 text-gray-400 hover:text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
title="Preview Certificate"
>
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
)} )}
@@ -3,29 +3,23 @@ import { useState } from 'react';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import CreateProjectModal from './CreateProjectDialog'; import CreateProjectModal from './CreateProjectDialog';
import { useProjects } from '../../../../hooks/useProjects'; import { useProjects } from '../../../../hooks/useProjects';
import * as LucideIcons from 'lucide-react'; import { getProjectIcon } from '../../../../lib/ProjectIcons';
import type { LucideProps } from 'lucide-react';
const getDynamicIcon = (iconName: string) => {
const formatted = iconName.charAt(0).toUpperCase() + iconName.slice(1);
const Icon = (LucideIcons as unknown as Record<string, React.ComponentType<LucideProps>>)[formatted];
return Icon ?? LucideIcons.FolderOpen;
};
const HomeMentorProject = () => { const HomeMentorProject = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { projects, loading, error } = useProjects(); const { projects, loading, error, refetch } = useProjects();
const openDialog = () => { const openDialog = () => { setIsModalOpen(true) };
setIsModalOpen(true); const closeDialog = () => { setIsModalOpen(false) };
};
const closeDialog = () => { const handleRefetch = () => {
setIsModalOpen(false); closeDialog();
}; refetch();
}
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex flex-row justify-between mb-3 px-5 items-center'> <div className='flex flex-row justify-between mb-3 items-center'>
<h2 className="text-lg font-semibold text-gray-800 mb-4">Projects</h2> <h2 className="text-lg font-semibold text-gray-800">Projects</h2>
<button <button
className='bg-card p-1.5 rounded-lg font-bold hover:bg-card-hover border border-card-outline' className='bg-card p-1.5 rounded-lg font-bold hover:bg-card-hover border border-card-outline'
onClick={openDialog} > onClick={openDialog} >
@@ -46,7 +40,7 @@ const HomeMentorProject = () => {
> >
<div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" style={{ backgroundColor: project.background_color ?? '#dc2626' }}> <div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" style={{ backgroundColor: project.background_color ?? '#dc2626' }}>
{(() => { {(() => {
const Icon = getDynamicIcon(project.project_icon); const Icon = getProjectIcon(project.project_icon);
return <Icon className="w-5 h-5 text-white" />; return <Icon className="w-5 h-5 text-white" />;
})()} })()}
</div> </div>
@@ -60,6 +54,7 @@ const HomeMentorProject = () => {
<CreateProjectModal <CreateProjectModal
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={closeDialog} onClose={closeDialog}
onSuccess={handleRefetch}
/> />
)} )}
</div> </div>
@@ -82,31 +82,31 @@ export default function InternsTable({
<img <img
src={intern.avatar} src={intern.avatar}
alt={intern.name} alt={intern.name}
className="w-10 h-10 rounded-full object-cover border border-gray-100 shadow-sm" className="w-10 h-10 rounded-full object-cover border border-gray-100 shadow-sm hidden md:block"
/> />
) : ( ) : (
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-emerald-500 flex items-center justify-center border border-gray-100 shadow-sm text-white text-xs font-bold"> <div className=" w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-emerald-500 items-center justify-center border border-gray-100 shadow-sm text-white text-xs font-bold hidden md:flex">
{intern.name.charAt(0)} {intern.name.charAt(0)}
</div> </div>
)} )}
<div> <div>
<h4 className="text-sm font-bold text-gray-900 leading-tight">{intern.name}</h4> <h4 className="text-xs md:text-sm font-bold text-gray-900 leading-tight">{intern.name}</h4>
<p className="text-xs text-gray-400 font-medium mt-0.5">{intern.email}</p> <p className="text-[10px] md:text-xs text-gray-400 font-medium mt-0.5">{intern.email}</p>
</div> </div>
</div> </div>
{/* Column 2: Current Project Title & Role Accent */} {/* Column 2: Current Project Title & Role Accent */}
<div className="w-1/3"> <div className="w-1/3">
<h5 className={`text-sm font-semibold leading-tight ${isUnassigned ? "text-gray-500 italic" : "text-gray-900"}`}> <h5 className={`text-xs md:text-sm font-semibold leading-tight ${isUnassigned ? "text-gray-500 italic" : "text-gray-900"}`}>
{intern.projectName || "Unassigned"} {intern.projectName || "Unassigned"}
</h5> </h5>
<p className={`text-xs font-bold mt-0.5 ${isUnassigned ? "text-red-500" : "text-red-600"}`}> <p className={`text-[10px] md:text-xs font-bold mt-0.5 ${isUnassigned ? "text-red-500" : "text-red-600"}`}>
{intern.role} {intern.role}
</p> </p>
</div> </div>
{/* Column 3: Action Buttons Block Right aligned */} {/* Column 3: Action Buttons Block Right aligned */}
<div className="w-1/6 flex items-center justify-end gap-3.5 pr-2"> <div className="w-1/6 flex flex-col md:flex-row items-center justify-end gap-3.5 pr-2">
{/* Tombol Add: membuka modal assign member untuk intern ini */} {/* Tombol Add: membuka modal assign member untuk intern ini */}
<button <button
onClick={() => onAssign(intern.id)} onClick={() => onAssign(intern.id)}
@@ -119,7 +119,7 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project
) : project.status === "completed" ? ( ) : project.status === "completed" ? (
<span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full">Completed</span> <span className="px-2 py-0.5 bg-blue-100 text-blue-800 rounded-full">Completed</span>
) : project.status === "archived" ? ( ) : project.status === "archived" ? (
<span className="px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded-full">Archived</span> <span className="px-2 py-0.5 bg-red-100 text-red-800 rounded-full">Archived</span>
) : ( ) : (
<span className="px-2 py-0.5 bg-gray-100 text-gray-800 rounded-full">Inactive</span> <span className="px-2 py-0.5 bg-gray-100 text-gray-800 rounded-full">Inactive</span>
)} )}
@@ -132,7 +132,7 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project
<button <button
onClick={handleCompleteButtonClick} onClick={handleCompleteButtonClick}
disabled={completeLoading} disabled={completeLoading}
className="p-1 rounded hover:text-green-600 hover:bg-green-50 transition-colors disabled:opacity-50" className="p-1 rounded hover:text-blue-600 hover:bg-blue-50 transition-colors disabled:opacity-50"
aria-label={`Selesaikan ${project.project_name}`} aria-label={`Selesaikan ${project.project_name}`}
title="Selesaikan project" title="Selesaikan project"
> >
@@ -143,7 +143,7 @@ export default function MentorProjectCard({ onArchived, onCompleted, ...project
<button <button
onClick={handleArchiveButtonClick} onClick={handleArchiveButtonClick}
disabled={archiveLoading} disabled={archiveLoading}
className="p-1 rounded hover:text-yellow-600 hover:bg-yellow-50 transition-colors disabled:opacity-50" className="p-1 rounded hover:text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
aria-label={`Arsipkan ${project.project_name}`} aria-label={`Arsipkan ${project.project_name}`}
title="Arsipkan project" title="Arsipkan project"
> >
@@ -1,5 +1,5 @@
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import type { ProjectTask } from "../../../../types/task.types"; import type { InternTaskItem } from "../../../../types/project.types";
const formatDate = (date: string) => const formatDate = (date: string) =>
new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); new Date(date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
@@ -7,7 +7,7 @@ const formatDate = (date: string) =>
const formatTime = (dateStr: string) => const formatTime = (dateStr: string) =>
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
export default function MentorTaskCard({ task }: {task: ProjectTask}) { export default function MentorTaskCard({ task }: {task: InternTaskItem}) {
const navigate = useNavigate(); const navigate = useNavigate();
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
@@ -31,8 +31,6 @@ export function StatusBadge({ status }: { status: DisplayStatus }) {
); );
} }
// StatusTable
interface StatusTableProps { interface StatusTableProps {
status: DisplayStatus; status: DisplayStatus;
submission: TaskSubmissionData | null; submission: TaskSubmissionData | null;
@@ -46,16 +44,6 @@ export function StatusTable({ status, submission }: StatusTableProps) {
label: "Submission status", label: "Submission status",
value: <StatusBadge status={status} />, value: <StatusBadge status={status} />,
}, },
{
label: "Grading status",
value: <span className="text-sm text-gray-400">-</span>,
},
{
label: "Time Remaining",
value: status === "overdue"
? <span className="text-sm text-red-600 font-medium">Overdue</span>
: <span className="text-sm text-gray-400">-</span>,
},
{ {
label: "Submitted at", label: "Submitted at",
value: submission?.submitted_at value: submission?.submitted_at
+2 -1
View File
@@ -25,10 +25,11 @@ import {
import type { MentorTaskItem } from "../types/task.types"; import type { MentorTaskItem } from "../types/task.types";
import { useCurrentUser } from "./useUser"; import { useCurrentUser } from "./useUser";
import { useMyTasks } from "./useProjects"; import { useMyTasks } from "./useProjects";
import type { InternTaskItem } from "../types/project.types";
// GET /task-api/projects/{id_project}/tasks // GET /task-api/projects/{id_project}/tasks
export const useProjectTasks = (projectId: number) => { export const useProjectTasks = (projectId: number) => {
const [tasks, setTasks] = useState<ProjectTask[]>([]); const [tasks, setTasks] = useState<InternTaskItem[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
+1 -1
View File
@@ -105,7 +105,7 @@ export const getMyTasks = async (): Promise<InternTaskItem[]> => {
return res.data.data; return res.data.data;
}; };
// GET /project-api/mentor-projects (admin/mentor only) // GET /project-api/mentor-projects (mentor only)
export const getMentorProjects = async (): Promise<Project[]> => { export const getMentorProjects = async (): Promise<Project[]> => {
const res = await api.get("/project-api/mentor-projects"); const res = await api.get("/project-api/mentor-projects");
return res.data.data; return res.data.data;
+2 -2
View File
@@ -7,7 +7,7 @@ import type {
UpdateTaskPayload, UpdateTaskPayload,
TaskSubmissionData, TaskSubmissionData,
} from "../types/task.types"; } from "../types/task.types";
import type { Project } from "../types/project.types"; import type { InternTaskItem, Project } from "../types/project.types";
import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types"; import type { MentorTaskItem, AdminTaskDetail } from "../types/task.types";
// ── Task ────────────────────────────────────────────── // ── Task ──────────────────────────────────────────────
@@ -22,7 +22,7 @@ export const createTask = async (
export const getProjectTasks = async ( export const getProjectTasks = async (
projectId: string | number projectId: string | number
): Promise<ProjectTask[]> => { ): Promise<InternTaskItem[]> => {
const res = await api.get(`/task-api/projects/${projectId}/tasks`); const res = await api.get(`/task-api/projects/${projectId}/tasks`);
return res.data.data; return res.data.data;
}; };