feat: update project task card to use new task type and improve navigation
fix: update project card to reflect actual tasks done from project data feat: enhance task form components to accept project ID for submissions refactor: clean up certificate detail page by removing unused elements chore: streamline certificate generation page by removing unnecessary navigation refactor: update certificate list page to use dynamic project data feat: improve interns management page with API integration and better state handling feat: add modal for assigning interns to projects with project selection feat: create a new component for displaying project cards in certificate list fix: correct import paths in create project dialog component refactor: enhance project hooks to include task filtering and refetch capabilities fix: update mock certificate data to reflect correct status feat: extend certificate types to include new fields for better tracking feat: add task_done field to project types for improved task management
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Download, Link, Printer } from "lucide-react";
|
||||
import { type CertificateData } from "../../../../lib/mockSertificates";
|
||||
import type { ProjectDetail } from "../../../../types/project.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
|
||||
export default function CertificateAvailable({ data }: { data: CertificateData }) {
|
||||
const { certificate } = data;
|
||||
interface CertificateAvailableProps {
|
||||
project: ProjectDetail | null;
|
||||
}
|
||||
|
||||
export default function CertificateAvailable({ project }: CertificateAvailableProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
@@ -13,10 +16,25 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
// Hitung durasi dari start_date - end_date
|
||||
const duration = project
|
||||
? (() => {
|
||||
const start = new Date(project.start_date);
|
||||
const end = new Date(project.end_date);
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
const months = Math.floor(diffDays / 30);
|
||||
const days = diffDays % 30;
|
||||
if (months > 0 && days > 0) return `${months} Bulan ${days} Hari`;
|
||||
if (months > 0) return `${months} Bulan`;
|
||||
return `${diffDays} Hari`;
|
||||
})()
|
||||
: "-";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Certificate Card */}
|
||||
<div className="flex rounded-xl p-5 mb-1 border border-gray-300 shadow-md items-center justify-center">
|
||||
<div className="flex rounded-xl p-5 mb-1 border bg-box-primary border-box-border shadow-md items-center justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
@@ -32,13 +50,14 @@ export default function CertificateAvailable({ data }: { data: CertificateData }
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Nama Program", value: certificate.programName },
|
||||
{ label: "Durasi", value: certificate.duration },
|
||||
{ label: "Nama Program", value: project?.project_name ?? "-" },
|
||||
{ label: "Durasi", value: duration },
|
||||
{ label: "Total Tugas", value: project ? `${project.total_tasks} Tugas` : "-" },
|
||||
{
|
||||
label: "Status",
|
||||
value: (
|
||||
<span className="bg-green-100 text-green-700 text-xs font-bold px-2 py-0.5 rounded-full">
|
||||
VERIFIED
|
||||
UNVERIFIED
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,38 +1,69 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { History } from "lucide-react";
|
||||
import { type CertificateHistory, history as certificateHistory } from "../../../../lib/mockSertificates";
|
||||
import { History, Loader2, AlertCircle } from "lucide-react";
|
||||
import type { Certificate } from "../../../../types/certificate.types";
|
||||
import certificateImg from "../../../../assets/certificate.png";
|
||||
import { useMyCertificates } from "../../../../hooks/useSertificates";
|
||||
|
||||
export default function SertificateHistory() {
|
||||
const navigate = useNavigate();
|
||||
const { certificates, loading, error } = useMyCertificates();
|
||||
const displayed = certificates.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5">
|
||||
<div className="bg-box-primary rounded-2xl border border-box-border shadow-sm p-5">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<History className="w-5 h-5 text-red-700" />
|
||||
<h3 className="font-semibold text-gray-800 text-lg">Certificate History</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{certificateHistory.slice(0, 2).map((item: CertificateHistory) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-full flex justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
className="rounded-xl h-[150px] w-[150px] w-full object-cover"
|
||||
/>
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-8 text-gray-400 gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span className="text-sm">Loading certificates...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && (
|
||||
<div className="flex items-center gap-2 py-6 text-red-500">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && displayed.length === 0 && (
|
||||
<p className="text-sm text-gray-400 py-6 text-center">No certificates yet.</p>
|
||||
)}
|
||||
|
||||
{!loading && !error && displayed.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{displayed.map((item: Certificate) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 p-3 bg-white border border-gray-200 shadow-sm rounded-xl hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<div className="w-full flex justify-center">
|
||||
<img
|
||||
src={certificateImg}
|
||||
alt="Certificate"
|
||||
className="rounded-xl h-[150px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p className="text-xs text-gray-400">
|
||||
{new Date(item.issued_at).toLocaleDateString("id-ID", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-tight">
|
||||
{item.project.project_name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">No: {item.certificate_no}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p className="text-xs text-gray-400">{item.month}</p>
|
||||
<p className="text-sm font-semibold text-gray-800 leading-tight">{item.title}</p>
|
||||
<p className="text-sm text-gray-500">Grade: {item.grade}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full my-3 flex justify-end">
|
||||
<button
|
||||
|
||||
@@ -1,43 +1,104 @@
|
||||
import { Lock, Clock } from "lucide-react";
|
||||
import { Lock, Award, Loader2 } from "lucide-react";
|
||||
|
||||
export default function CertificateNotAvailable({ progress, remainingTasks }: { progress: number; remainingTasks: number }) {
|
||||
interface CertificateNotAvailableProps {
|
||||
progress: number;
|
||||
remainingTasks: number;
|
||||
allTasksDone?: boolean;
|
||||
onClaim?: () => void;
|
||||
claiming?: boolean;
|
||||
}
|
||||
|
||||
export default function CertificateNotAvailable({
|
||||
progress,
|
||||
remainingTasks,
|
||||
allTasksDone = false,
|
||||
onClaim,
|
||||
claiming = false,
|
||||
}: CertificateNotAvailableProps) {
|
||||
return (
|
||||
<div className="bg-white w-full h-full rounded-2xl border border-gray-100 shadow-sm px-24 flex flex-col items-center justify-center">
|
||||
<div className="bg-box-primary w-full h-full rounded-2xl border border-box-border shadow-sm px-24 py-12 flex flex-col items-center justify-center">
|
||||
{/* Icon */}
|
||||
<div className="relative mb-6">
|
||||
<div className=" w-45 h-45 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||
|
||||
<div className="w-30 h-30 flex items-center rounded-lg justify-center bg-gray-300">
|
||||
<Lock className="w-16 h-16 text-red-700" />
|
||||
</div>
|
||||
<div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
|
||||
<Clock className="w-5 h-5 text-yellow-800" />
|
||||
<div className="w-34 h-34 border-2 border-dashed border-gray-600 rounded-xl flex justify-center items-center">
|
||||
<div className="w-26 h-26 flex items-center rounded-lg justify-center bg-gray-300">
|
||||
{allTasksDone ? (
|
||||
<Award className="w-12 h-12 text-green-600" />
|
||||
) : (
|
||||
<Lock className="w-12 h-12 text-red-700" />
|
||||
)}
|
||||
</div>
|
||||
{/* <div className="absolute -bottom-2 -right-2 w-7 h-7 bg-amber-500 rounded- flex items-center justify-center shadow-md">
|
||||
<Clock className="w-4 h-4 text-yellow-800" />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
|
||||
<p className="text-gray-500 text-[17px] mb-6">
|
||||
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
|
||||
Silakan periksa kembali progres tugas Anda di menu Proyek.
|
||||
</p>
|
||||
{allTasksDone ? (
|
||||
<>
|
||||
<h2 className="text-[37px] font-bold text-gray-900 mb-3">Selamat!</h2>
|
||||
<p className="text-gray-500 text-[17px] mb-6 text-center">
|
||||
Semua tugas proyek telah selesai. Klik tombol di bawah untuk{" "}
|
||||
<span className="font-bold text-green-600">mengklaim sertifikat</span> kelulusan Anda.
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-1/3">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-red-700">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-700 rounded-full transition-all duration-700"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 text-left">
|
||||
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
|
||||
</p>
|
||||
</div>
|
||||
{/* Progress Bar — 100% */}
|
||||
<div className="w-1/3 mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-green-600">100%</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-green-500 rounded-full transition-all duration-700 w-full" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-2 text-left">Semua tugas telah diselesaikan!</p>
|
||||
</div>
|
||||
|
||||
{/* Tombol Claim */}
|
||||
<button
|
||||
onClick={onClaim}
|
||||
disabled={claiming}
|
||||
className="flex items-center justify-center gap-2 px-8 py-3 bg-red-700 hover:bg-red-800 disabled:opacity-60 active:scale-95 transition-all text-white font-semibold rounded-xl text-sm"
|
||||
>
|
||||
{claiming ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Mengklaim...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Award className="w-4 h-4" />
|
||||
Claim Certificate
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h2 className="text-[28px] font-bold text-gray-900 mb-3">Sertifikat Belum Tersedia</h2>
|
||||
<p className="text-gray-500 text-[15px] mb-6 text-center">
|
||||
Anda harus menyelesaikan <span className="font-bold text-red-600">semua tugas proyek</span> sebelum dapat mengunduh sertifikat kelulusan.
|
||||
Silakan periksa kembali progres tugas Anda di menu Proyek.
|
||||
</p>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="w-1/3">
|
||||
<p className="text-xs text-font mt-2">
|
||||
Selesaikan {remainingTasks} tugas lagi untuk membuka akses.
|
||||
</p>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-semibold text-gray-700">Progres Proyek</span>
|
||||
<span className="text-sm font-bold text-red-700">{progress}%</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-red-700 rounded-full transition-all duration-700"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { ProjectTask } from "../../../../types/task.types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import type { InternTaskItem } from "../../../../types/project.types";
|
||||
|
||||
const formatDate = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
@@ -7,37 +7,36 @@ const formatDate = (dateStr: string) =>
|
||||
const formatTime = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
export default function TaskCard({ task }: { task: ProjectTask }) {
|
||||
export default function TaskCard({ task }: { task: InternTaskItem }) {
|
||||
const navigate = useNavigate();
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
return (
|
||||
<div
|
||||
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3"
|
||||
onClick={() => navigate(`/intern/projects/${slug}/${task.slug}?type=${task.submission_type}`)}
|
||||
className="border border-card-outline rounded-xl px-6 py-4 mb-3 bg-white flex items-start justify-between gap-3 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
onClick={() => navigate(`/intern/projects/${task.project_slug}/${task.slug}?type=${task.submission_type}`)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className=" text-[11px] text-font mb-1">
|
||||
<div className="text-[11px] text-font mb-1">
|
||||
Deadline : {formatDate(task.deadline_at)} at {formatTime(task.deadline_at)}
|
||||
</div>
|
||||
<div className=" font-bold text-[15px] text-[#1a1a1a] mb-1">
|
||||
<div className="font-bold text-[15px] text-[#1a1a1a] mb-1">
|
||||
{task.title}
|
||||
</div>
|
||||
<div className=" text-[13px] text-font">
|
||||
<div className="text-[13px] text-font">
|
||||
{task.description}
|
||||
</div>
|
||||
<div className=" text-[13px] text-font">
|
||||
<div className="text-[13px] text-font mt-0.5">
|
||||
Tipe Submission : {task.submission_type === 'file_upload' ? "File" : "Link"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="block md:hidden h-[10px] bg-gray-600"/>
|
||||
|
||||
<div className="shrink-0 hidden md:block">
|
||||
<button
|
||||
className="bg-white text-font-shade border-[1.5px] border-card-outline font-semibold text-xs px-3.5 py-[5px] rounded-md cursor-pointer whitespace-nowrap transition-colors duration-150"
|
||||
<div className="shrink-0 hidden md:flex items-center">
|
||||
<span
|
||||
className={`font-semibold text-xs px-3.5 py-[5px] rounded-md whitespace-nowrap bg-gray-50`}
|
||||
>
|
||||
Add Submission
|
||||
</button>
|
||||
{task.submission_status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ const ProjectCard = (project: Project) => {
|
||||
const Icon = getDynamicIcon(project.project_icon);
|
||||
|
||||
|
||||
const tasksDone = 1;
|
||||
const tasksDone = project.task_done;
|
||||
const tasksTotal = project.total_tasks;
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
|
||||
interface TaskFormFileProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
deadline?: Date;
|
||||
initialSubmission?: TaskSubmissionData | null;
|
||||
}
|
||||
|
||||
export default function TaskFormFile({ taskId, deadline, initialSubmission }: TaskFormFileProps) {
|
||||
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission);
|
||||
export default function TaskFormFile({ taskId, projectId, deadline, initialSubmission }: TaskFormFileProps) {
|
||||
const { submission, loading, submitFile, updateFile, remove } = useSubmission(taskId, initialSubmission, projectId);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isDragActive, setIsDragActive] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
@@ -7,12 +7,13 @@ import type { TaskSubmissionData } from "../../../../types/task.types";
|
||||
|
||||
interface TaskFormLinkProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
deadline?: Date;
|
||||
initialSubmission?: TaskSubmissionData | null;
|
||||
}
|
||||
|
||||
export default function TaskFormLink({ taskId, deadline, initialSubmission }: TaskFormLinkProps) {
|
||||
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission);
|
||||
export default function TaskFormLink({ taskId, projectId, deadline, initialSubmission }: TaskFormLinkProps) {
|
||||
const { submission, loading, submitLink, updateLink, remove } = useSubmission(taskId, initialSubmission, projectId);
|
||||
const [inputLink, setInputLink] = useState(initialSubmission?.url_link ?? "");
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user