feat: add notification system with real-time updates, certificate generator utility, and intern eligibility tracking components
This commit is contained in:
@@ -20,7 +20,8 @@ export default function NotificationList() {
|
||||
const loadNotifications = async () => {
|
||||
try {
|
||||
const data = await NotificationService.getNotifications();
|
||||
const mapped = data.map(NotificationService.mapNotificationToUI);
|
||||
const unread = data.filter((n) => !n.is_read);
|
||||
const mapped = unread.map(NotificationService.mapNotificationToUI);
|
||||
setNotifications(mapped);
|
||||
} catch (err) {
|
||||
console.error("Failed to load notifications:", err);
|
||||
@@ -44,6 +45,7 @@ export default function NotificationList() {
|
||||
|
||||
socket.on("notification", (notif: BackendNotification) => {
|
||||
console.log("New realtime notification received:", notif);
|
||||
if (notif.is_read) return;
|
||||
const mapped = NotificationService.mapNotificationToUI(notif);
|
||||
setNotifications(prev => {
|
||||
if (prev.some(p => p.id === mapped.id)) {
|
||||
@@ -66,7 +68,7 @@ export default function NotificationList() {
|
||||
const handleMarkAllRead = async () => {
|
||||
try {
|
||||
await NotificationService.markAllAsRead();
|
||||
setNotifications(prev => prev.map(n => ({ ...n, isNew: false })));
|
||||
setNotifications([]);
|
||||
alert("All notifications marked as read!");
|
||||
} catch (err) {
|
||||
console.error("Failed to mark all as read:", err);
|
||||
@@ -130,16 +132,16 @@ export default function NotificationList() {
|
||||
</p>
|
||||
|
||||
{/* Conditional Actions */}
|
||||
{item.type === 'new-task' && (
|
||||
{item.type === 'new-task' && item.link &&(
|
||||
<div className="flex items-center gap-2 pt-3">
|
||||
<button
|
||||
className="px-4 py-1.5 bg-[#B30000] hover:bg-[#990000] text-white text-xs font-bold rounded-lg shadow-sm transition-colors"
|
||||
onClick={() => navigate(`/${item.link}`)}>
|
||||
onClick={() => navigate(`${item.link}`)}>
|
||||
View Task
|
||||
</button>
|
||||
<button className="px-4 py-1.5 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors">
|
||||
{/* <button className="px-4 py-1.5 border border-gray-200 text-gray-500 hover:text-gray-700 hover:bg-gray-50 text-xs font-semibold rounded-lg transition-colors">
|
||||
Later
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,7 +150,7 @@ export default function NotificationList() {
|
||||
className="flex items-center gap-1 text-xs font-bold text-[#B30000] hover:underline pt-2"
|
||||
onClick={() => navigate(`/certificates`)}>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
Download Certificate
|
||||
Ke Halaman
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,20 @@ interface EligibleInternTableProps {
|
||||
|
||||
const PER_PAGE = 5;
|
||||
|
||||
const BULAN_ID = [
|
||||
"Januari", "Februari", "Maret", "April", "Mei", "Juni",
|
||||
"Juli", "Agustus", "September", "Oktober", "November", "Desember",
|
||||
];
|
||||
|
||||
function formatDateIndo(dateStr: string): string {
|
||||
const [year, month, day] = dateStr.split("-").map(Number);
|
||||
return `${day} ${BULAN_ID[month - 1]} ${year}`;
|
||||
}
|
||||
|
||||
function formatDateRange(startDate: string, endDate: string): string {
|
||||
return `${formatDateIndo(startDate)} - ${formatDateIndo(endDate)}`;
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: boolean }) {
|
||||
return (
|
||||
<div className={`flex items-center gap-1 text-[11px] font-extrabold tracking-wide ${status ? "text-green-600" : "text-orange-500"}`}>
|
||||
@@ -49,7 +63,6 @@ export default function EligibleInternTable({
|
||||
// Durasi tanggal Proyek
|
||||
}: EligibleInternTableProps) {
|
||||
const navigate = useNavigate();
|
||||
// const { project } = useProjectDetail(String(projectId));
|
||||
const { generate: SubmitCertificates } = useGenerateCertificates();
|
||||
|
||||
const initialSelected = new Set(
|
||||
@@ -99,6 +112,8 @@ export default function EligibleInternTable({
|
||||
const zip = new JSZip();
|
||||
const templateUrl = resolveImageUrl(project.certificate_template!);
|
||||
|
||||
const duration = formatDateRange(project.start_date, project.end_date);
|
||||
|
||||
await Promise.all(
|
||||
result.map(async (cert) => {
|
||||
const verifyUrl = `${window.location.origin}/verify-certificate/${cert.uuid}`;
|
||||
@@ -108,7 +123,7 @@ export default function EligibleInternTable({
|
||||
cert.project.project_name,
|
||||
cert.certificate_no,
|
||||
verifyUrl,
|
||||
"20 Juni 2024 - 20 September 2024"
|
||||
duration
|
||||
);
|
||||
zip.file(`Sertifikat - ${cert.user.full_name}.png`, blob);
|
||||
})
|
||||
|
||||
@@ -80,9 +80,9 @@ export async function generateCertificate(
|
||||
ctx.fillText(certificateNo, canvas.width * 0.825, canvas.height * 0.918);
|
||||
|
||||
//Durasi Project — besar (Grenze)
|
||||
ctx.font = `50px "Grenze", serif`;
|
||||
ctx.font = `400 48px "Grenze", serif`;
|
||||
ctx.fillStyle = "#090909";
|
||||
ctx.fillText(ProjectDuration, canvas.width * 0.57, canvas.height * 0.715);
|
||||
ctx.fillText("20 April - 20 Juni 2025", canvas.width * 0.68, canvas.height * 0.66);
|
||||
|
||||
// QR code — kalau verifyUrl diisi
|
||||
const qrDataUrl = await QRCode.toDataURL('http://localhost:5172/verify-certificate/' + verifyUrl, {
|
||||
@@ -93,8 +93,8 @@ export async function generateCertificate(
|
||||
const qrSize = canvas.width * 0.103;
|
||||
ctx.drawImage(
|
||||
qrImg,
|
||||
canvas.width * 0.77 - qrSize,
|
||||
canvas.height * 0.715 - qrSize,
|
||||
canvas.width * 0.8 - qrSize,
|
||||
canvas.height * 0.75 - qrSize,
|
||||
qrSize,
|
||||
qrSize,
|
||||
);
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
ctx.fillStyle = "#090909";
|
||||
ctx.fillText("Internify Platform Dev", canvas.width * 0.57, canvas.height * 0.615);
|
||||
|
||||
ctx.font = `400 48px "Grenze", serif`;
|
||||
ctx.fillStyle = "#090909";
|
||||
ctx.fillText("20 April - 20 Juni 2025", canvas.width * 0.68, canvas.height * 0.66);
|
||||
|
||||
// 4. Certificate no
|
||||
ctx.font = `50px Grenze`;
|
||||
ctx.fillText("CERT/20260609/PRJ2/USR5", canvas.width * 0.665, canvas.height * 0.305);
|
||||
|
||||
@@ -90,6 +90,7 @@ export const mapNotificationToUI = (notif: BackendNotification): UINotification
|
||||
title: notif.title,
|
||||
time: formatRelativeTime(notif.created_at),
|
||||
isNew: !notif.is_read,
|
||||
link: notif.link ?? "",
|
||||
group: mapGroup(notif.created_at),
|
||||
description: notif.message,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface BackendNotification {
|
||||
message: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
link: string | null;
|
||||
}
|
||||
|
||||
export interface UINotification {
|
||||
|
||||
Reference in New Issue
Block a user