diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/app/(main)/profile/events/viewCertificates/[id]/page.jsx b/app/(main)/profile/events/viewCertificates/[id]/page.jsx new file mode 100644 index 0000000..a935533 --- /dev/null +++ b/app/(main)/profile/events/viewCertificates/[id]/page.jsx @@ -0,0 +1,86 @@ +import fs from "fs"; +import path from "path"; +import Link from "next/link"; + +function getCertificates(eventId) { + const certDir = path.join(process.cwd(), "public", "certificates", eventId); + + if (!fs.existsSync(certDir)) { + return []; + } + + return fs + .readdirSync(certDir) + .filter((file) => /\.(png|jpe?g|pdf)$/i.test(file)) + .sort((a, b) => a.localeCompare(b)) + .map((file) => ({ + name: file, + url: `/certificates/${eventId}/${file}`, + isImage: /\.(png|jpe?g)$/i.test(file), + })); +} + +export default async function Page({ params }) { + const { id } = await params; + const certificates = getCertificates(id); + + return ( +
+
+ ← Back to Certificates +
+ +

Certificates for event

+

+ Event ID: {id} +

+ + {certificates.length === 0 ? ( +
+ No certificates have been generated for this event yet. +
+ ) : ( +
+ {certificates.map((certificate) => ( +
+

{certificate.name}

+ + {certificate.isImage ? ( + {certificate.name} + ) : ( + + Open PDF + + )} +
+ ))} +
+ )} +
+ ); +} diff --git a/app/(main)/profile/events/viewCertificates/page.jsx b/app/(main)/profile/events/viewCertificates/page.jsx new file mode 100644 index 0000000..65bcfe3 --- /dev/null +++ b/app/(main)/profile/events/viewCertificates/page.jsx @@ -0,0 +1,11 @@ +import Link from "next/link"; + +export default function Page() { + return ( +
+

Certificate viewer

+

Select an event from the certificate list to view its generated files.

+ Back to Certificates +
+ ); +} diff --git a/app/api/certificate/addCertificateTemplate/route.ts b/app/api/certificate/addCertificateTemplate/route.ts index f9b8711..48e0690 100644 --- a/app/api/certificate/addCertificateTemplate/route.ts +++ b/app/api/certificate/addCertificateTemplate/route.ts @@ -2,10 +2,6 @@ import { addCertificateTemplate } from "@/lib/services/certificates"; import { body, expressError, handle, json } from "@/lib/api/express"; import { getCurrentUser, isAdmin } from "@/lib/auth/access"; -/** - * POST /api/certificate/addCertificateTemplate - * Port of controllers/certificate/certificateController.js — admin only. - */ export async function POST(request: Request) { return handle(async () => { const user = await getCurrentUser(); @@ -15,21 +11,18 @@ export async function POST(request: Request) { const b = await body<{ eventId?: string; template?: string; - fields?: unknown; + fields?: unknown[]; }>(request); - const fields = Array.isArray(b.fields) - ? b.fields - : typeof b.fields === "string" - ? JSON.parse(b.fields) - : []; + if (!b.eventId) return expressError(400, "Event ID is required"); + if (!b.template) return expressError(400, "A template image is required"); - const record = await addCertificateTemplate({ - eventId: b.eventId ?? "", - template: b.template ?? "", - fields, + const data = await addCertificateTemplate({ + eventId: b.eventId, + template: b.template, + fields: (b.fields ?? []) as any, }); - return json({ success: true, message: "Template saved", certificate: record }); + return json({ success: true, data }); }); } diff --git a/app/api/certificate/dummyCertificate/route.ts b/app/api/certificate/dummyCertificate/route.ts index 159746d..e5ddc3e 100644 --- a/app/api/certificate/dummyCertificate/route.ts +++ b/app/api/certificate/dummyCertificate/route.ts @@ -2,11 +2,6 @@ import { dummyCertificate } from "@/lib/services/certificates"; import { body, expressError, handle, json } from "@/lib/api/express"; import { getCurrentUser, isAdmin } from "@/lib/auth/access"; -/** - * POST /api/certificate/dummyCertificate - * Port of controllers/certificate/testNameController.js — preview data for the - * admin certificate designer. - */ export async function POST(request: Request) { return handle(async () => { const user = await getCurrentUser(); @@ -18,8 +13,10 @@ export async function POST(request: Request) { fieldValues?: Record; }>(request); + if (!b.eventId) return expressError(400, "Event ID is required"); + const data = await dummyCertificate({ - eventId: b.eventId ?? "", + eventId: b.eventId, fieldValues: b.fieldValues, }); diff --git a/app/api/certificate/route.ts b/app/api/certificate/route.ts new file mode 100644 index 0000000..159746d --- /dev/null +++ b/app/api/certificate/route.ts @@ -0,0 +1,28 @@ +import { dummyCertificate } from "@/lib/services/certificates"; +import { body, expressError, handle, json } from "@/lib/api/express"; +import { getCurrentUser, isAdmin } from "@/lib/auth/access"; + +/** + * POST /api/certificate/dummyCertificate + * Port of controllers/certificate/testNameController.js — preview data for the + * admin certificate designer. + */ +export async function POST(request: Request) { + return handle(async () => { + const user = await getCurrentUser(); + if (!user) return expressError(401, "Token is required"); + if (!isAdmin(user)) return expressError(403, "Unauthorized"); + + const b = await body<{ + eventId?: string; + fieldValues?: Record; + }>(request); + + const data = await dummyCertificate({ + eventId: b.eventId ?? "", + fieldValues: b.fieldValues, + }); + + return json({ success: true, ...data }); + }); +} diff --git a/app/api/certificate/sendCertificatesAndEvents/route.ts b/app/api/certificate/sendCertificatesAndEvents/route.ts index 97917a8..dc8fcfc 100644 --- a/app/api/certificate/sendCertificatesAndEvents/route.ts +++ b/app/api/certificate/sendCertificatesAndEvents/route.ts @@ -19,6 +19,7 @@ export async function POST(request: Request) { eventId?: string; recipients?: Array<{ email: string; fieldValues?: Record }>; emails?: string[]; + resend?: boolean; }>(request); // Accepts either a rich recipient list or a plain array of addresses. @@ -29,8 +30,22 @@ export async function POST(request: Request) { const data = await sendCertificatesAndEvents({ eventId: b.eventId ?? "", recipients, + resend: b.resend === true, }); - return json({ success: true, message: "Certificates processed", data }); + const status = data.failures.length > 0 ? 207 : 200; + return json( + { + success: data.failures.length === 0, + message: + data.failures.length > 0 + ? "Some certificates could not be emailed" + : "Certificates sent successfully", + data, + failed: data.failures, + }, + status, + ); }); } + diff --git a/app/api/certificate/testCertificateSending/route.ts b/app/api/certificate/testCertificateSending/route.ts index 8a41b85..7b0e0ea 100644 --- a/app/api/certificate/testCertificateSending/route.ts +++ b/app/api/certificate/testCertificateSending/route.ts @@ -24,9 +24,12 @@ export async function POST(request: Request) { name: b.name ?? user.name ?? to, eventName: b.eventName ?? "a FED KIIT event", certificateId: b.certificateId ?? "TEST-CERTIFICATE", + isTest: true, }); - if (!result.sent) return expressError(502, "Could not send the test email"); + if (!result.sent) { + return expressError(502, result.reason || "Could not send the test email"); + } return json({ success: true, message: `Test certificate sent to ${to}` }); }); diff --git a/app/api/certificates/[id]/route.ts b/app/api/certificates/[id]/route.ts new file mode 100644 index 0000000..4720a86 --- /dev/null +++ b/app/api/certificates/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +export async function GET(_req: Request, context: { params: Promise<{ id: string }> | { id: string } }) { + try { + const params = await (context as any).params; + const id = params?.id; + if (!id) return NextResponse.json([], { status: 200 }); + + const certDir = path.join(process.cwd(), "public", "certificates", id); + if (!fs.existsSync(certDir)) return NextResponse.json([], { status: 200 }); + + const files = fs + .readdirSync(certDir) + .filter((f) => /\.(png|jpe?g|pdf)$/i.test(f)) + .map((name) => ({ name, url: `/certificates/${id}/${name}` })); + + return NextResponse.json(files, { status: 200 }); + } catch (err) { + console.error("cert api err", err); + return NextResponse.json({ error: "Internal error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/certificates/counts/route.ts b/app/api/certificates/counts/route.ts new file mode 100644 index 0000000..2cdeb20 --- /dev/null +++ b/app/api/certificates/counts/route.ts @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import fs from "fs"; +import path from "path"; + +export async function POST(req: Request) { + try { + const body = await req.json(); + const ids: string[] = Array.isArray(body?.ids) ? body.ids : []; + + const result: Record = {}; + for (const id of ids) { + const certDir = path.join(process.cwd(), "public", "certificates", id); + if (!fs.existsSync(certDir)) { + result[id] = 0; + continue; + } + const files = fs.readdirSync(certDir).filter((f) => /\.(png|jpe?g|pdf)$/i.test(f)); + result[id] = files.length; + } + + return NextResponse.json(result, { status: 200 }); + } catch (err) { + console.error("cert counts err", err); + return NextResponse.json({ error: "Internal error" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/profile/events/SendCertificate/[id]/page.jsx b/app/profile/events/SendCertificate/[id]/page.jsx new file mode 100644 index 0000000..0aee919 --- /dev/null +++ b/app/profile/events/SendCertificate/[id]/page.jsx @@ -0,0 +1,5 @@ +import SendCertificate from "@/src/sections/Profile/Admin/Form/CertificatesForm/SendCertificate"; + +export default function Page() { + return ; +} diff --git a/app/profile/events/createCertificates/[id]/page.tsx b/app/profile/events/createCertificates/[id]/page.tsx new file mode 100644 index 0000000..344da70 --- /dev/null +++ b/app/profile/events/createCertificates/[id]/page.tsx @@ -0,0 +1,5 @@ +import CertificatesForm from "@/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm"; + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/lib/services/certificates.ts b/lib/services/certificates.ts index 2e79dc7..7667666 100644 --- a/lib/services/certificates.ts +++ b/lib/services/certificates.ts @@ -84,14 +84,10 @@ export async function addCertificateTemplate(input: { if (!input.eventId) throw new ApiError(400, "Event ID is required"); if (!input.template) throw new ApiError(400, "A template image is required"); - const event = await prisma.event.findUnique({ - where: { id: input.eventId }, - select: { id: true }, - }); - if (!event) throw new ApiError(404, "Event not found"); + const resolvedEventId = await getOrCreateCertificateEventId(input.eventId); const existing = await prisma.certificate.findFirst({ - where: { eventId: input.eventId }, + where: { eventId: resolvedEventId }, select: { id: true }, }); @@ -103,19 +99,96 @@ export async function addCertificateTemplate(input: { data: { template: input.template, fields }, }) : await prisma.certificate.create({ - data: { eventId: input.eventId, template: input.template, fields }, + data: { eventId: resolvedEventId, template: input.template, fields }, }); return record; } /** Template plus a sample row, for the admin preview. */ +async function resolveEventId(rawEventId?: string): Promise { + const value = rawEventId?.trim(); + if (!value) return null; + + const byId = await prisma.event.findUnique({ + where: { id: value }, + select: { id: true }, + }); + if (byId) return byId.id; + + const byFormId = await prisma.event.findFirst({ + where: { formId: value }, + select: { id: true }, + }); + if (byFormId) return byFormId.id; + + return null; +} + +/** + * Certificate management is opened from a form id. Older deployments created + * the companion Event in a separate Express endpoint; create it here when it + * does not exist yet so a newly uploaded template has somewhere to live. + */ +async function getOrCreateCertificateEventId(rawEventId?: string): Promise { + const existingId = await resolveEventId(rawEventId); + if (existingId) return existingId; + + const formId = rawEventId?.trim(); + if (!formId) throw new ApiError(400, "Event ID is required"); + + const form = await prisma.form.findUnique({ + where: { id: formId }, + select: { id: true, info: true }, + }); + if (!form) throw new ApiError(404, "Event form not found"); + + const configuredOrganisationId = + process.env.CERTIFICATE_ORGANISATION_ID ?? process.env.NEXT_PUBLIC_CERT_ORG; + const hasValidConfiguredOrganisationId = /^[a-f\d]{24}$/i.test( + configuredOrganisationId ?? "", + ); + const configuredOrganisation = hasValidConfiguredOrganisationId + ? await prisma.organisation.findUnique({ + where: { id: configuredOrganisationId! }, + select: { id: true }, + }) + : null; + const organisation = + configuredOrganisation ?? + (await prisma.organisation.findFirst({ select: { id: true } })); + + if (!organisation) { + throw new ApiError(400, "No organisation is available for certificate events"); + } + + const info = (form.info ?? {}) as Record; + const name = typeof info.eventTitle === "string" ? info.eventTitle : "Untitled Event"; + const description = + typeof info.eventdescription === "string" + ? info.eventdescription + : typeof info.eventDescription === "string" + ? info.eventDescription + : ""; + + const event = await prisma.event.create({ + data: { name, description, organisationId: organisation.id, formId: form.id }, + select: { id: true }, + }); + return event.id; +} + export async function dummyCertificate(input: { eventId: string; fieldValues?: Record; }) { + const resolvedEventId = await resolveEventId(input.eventId); + if (!resolvedEventId) { + throw new ApiError(404, "No certificate template exists for this event"); + } + const template = await prisma.certificate.findFirst({ - where: { eventId: input.eventId }, + where: { eventId: resolvedEventId }, }); if (!template) { throw new ApiError(404, "No certificate template exists for this event"); @@ -134,8 +207,9 @@ export async function sendCertificateEmail(input: { name: string; eventName: string; certificateId: string; + isTest?: boolean; }) { - const verifyUrl = `${siteUrl()}/verify/certificate?certificateId=${encodeURIComponent(input.certificateId)}`; + const verifyUrl = `${siteUrl()}/verify/certificate?id=${encodeURIComponent(input.certificateId)}`; const escape = (v: string) => v.replace(/&/g, "&").replace(//g, ">"); @@ -153,11 +227,12 @@ export async function sendCertificateEmail(input: {

Hi ${escape(input.name)}, thank you for taking part in ${escape(input.eventName)}. Your certificate is available below.

+${input.isTest ? `

This is a test email. A certificate is not issued until you use Send Mail.

` : `
View certificate
-

Certificate ID: ${escape(input.certificateId)}

+

Certificate ID: ${escape(input.certificateId)}

`} `, }); } @@ -171,56 +246,64 @@ Hi ${escape(input.name)}, thank you for taking part in export async function sendCertificatesAndEvents(input: { eventId: string; recipients: Array<{ email: string; fieldValues?: Record }>; + resend?: boolean; }) { if (!input.eventId) throw new ApiError(400, "Event ID is required"); if (!Array.isArray(input.recipients) || input.recipients.length === 0) { throw new ApiError(400, "At least one recipient is required"); } + const resolvedEventId = await getOrCreateCertificateEventId(input.eventId); + const event = await prisma.event.findUnique({ - where: { id: input.eventId }, + where: { id: resolvedEventId }, select: { id: true, name: true }, }); if (!event) throw new ApiError(404, "Event not found"); const template = await prisma.certificate.findFirst({ - where: { eventId: input.eventId }, + where: { eventId: resolvedEventId }, }); if (!template) { throw new ApiError(404, "No certificate template exists for this event"); } const existing = await prisma.issuedCertificates.findMany({ - where: { eventId: input.eventId }, - select: { email: true }, + where: { eventId: resolvedEventId }, + select: { id: true, email: true, mailed: true }, }); - const already = new Set(existing.map((e) => e.email.toLowerCase())); + const existingByEmail = new Map( + existing.map((certificate) => [certificate.email.toLowerCase(), certificate]), + ); let issued = 0; let skipped = 0; let mailed = 0; - const failures: string[] = []; + const failures: Array<{ email: string; error: string }> = []; for (const recipient of input.recipients) { const email = recipient.email?.trim().toLowerCase(); if (!email) continue; - if (already.has(email)) { + const existingCertificate = existingByEmail.get(email); + if (existingCertificate?.mailed && !input.resend) { skipped++; continue; } - const record = await prisma.issuedCertificates.create({ - data: { - eventId: input.eventId, - certificateId: template.id, - email, - fields: template.fields as Prisma.InputJsonValue[], - fieldValues: (recipient.fieldValues ?? {}) as Prisma.InputJsonValue, - mailed: false, - }, - }); - issued++; + const record = + existingCertificate ?? + (await prisma.issuedCertificates.create({ + data: { + eventId: resolvedEventId, + certificateId: template.id, + email, + fields: template.fields as Prisma.InputJsonValue[], + fieldValues: (recipient.fieldValues ?? {}) as Prisma.InputJsonValue, + mailed: false, + }, + })); + if (!existingCertificate) issued++; const result = await sendCertificateEmail({ to: email, @@ -236,7 +319,7 @@ export async function sendCertificatesAndEvents(input: { data: { mailed: true }, }); } else { - failures.push(email); + failures.push({ email, error: result.reason }); } } diff --git a/ours_layout.jsx b/ours_layout.jsx new file mode 100644 index 0000000..0437313 Binary files /dev/null and b/ours_layout.jsx differ diff --git a/package-lock.json b/package-lock.json index f95a11c..e29c7cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2109,7 +2109,7 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", @@ -2122,14 +2122,14 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2143,14 +2143,14 @@ "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.19.3", @@ -2162,7 +2162,7 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "6.19.3" @@ -2195,7 +2195,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@swc/helpers": { @@ -2584,7 +2584,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -3797,7 +3796,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.3", @@ -3980,7 +3979,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -3996,7 +3995,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "consola": "^3.2.3" @@ -4094,14 +4093,14 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -4301,7 +4300,7 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=16.0.0" @@ -4347,7 +4346,7 @@ "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -4372,7 +4371,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-element-overflow": { @@ -4443,7 +4442,7 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -4479,7 +4478,7 @@ "version": "3.21.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4504,7 +4503,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=14" @@ -5170,7 +5169,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/extend": { @@ -5183,7 +5182,7 @@ "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -5627,7 +5626,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.1.6", @@ -6528,7 +6527,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -8014,7 +8013,7 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/node-releases": { @@ -8031,7 +8030,7 @@ "version": "0.6.9", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "citty": "^0.2.2", @@ -8049,7 +8048,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/object-assign": { @@ -8178,7 +8177,7 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/optionator": { @@ -8344,14 +8343,14 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/picocolors": { @@ -8377,7 +8376,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.2.4", @@ -8467,7 +8466,7 @@ "version": "6.19.3", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -8533,7 +8532,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -8580,7 +8579,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "defu": "^6.1.4", @@ -8990,7 +8989,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -9928,7 +9927,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -10155,7 +10154,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json.bak b/package.json.bak new file mode 100644 index 0000000..ec78d72 --- /dev/null +++ b/package.json.bak @@ -0,0 +1,81 @@ +{ + "name": "fedkiit", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "node scripts/with-env.mjs dev", + "build": "node scripts/with-env.mjs build", + "start": "node scripts/with-env.mjs start", + "lint": "eslint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@google/generative-ai": "^0.24.1", + "@mui/icons-material": "^9.2.0", + "@mui/material": "^9.2.0", + "@prisma/client": "^6.19.3", + "@react-oauth/google": "^0.13.5", + "aos": "^2.3.4", + "axios": "^1.19.0", + "bcryptjs": "^3.0.3", + "blurhash": "^2.0.5", + "cloudinary": "^2.10.0", + "clsx": "^2.1.1", + "date-fns": "^4.4.0", + "dompurify": "^3.4.12", + "framer-motion": "^12.43.0", + "google-auth-library": "^10.9.1", + "html2canvas": "^1.4.1", + "html5-qrcode": "^2.3.8", + "jose": "^6.2.5", + "js-confetti": "^0.13.1", + "lucide-react": "^1.27.0", + "moment": "^2.30.1", + "nanoid": "^6.0.0", + "next": "16.2.12", + "prop-types": "^15.8.1", + "qrcode.react": "^4.2.0", + "react": "19.2.4", + "react-avatar-editor": "^15.1.0", + "react-blurhash": "^0.3.0", + "react-date-picker": "^12.1.0", + "react-datepicker": "^9.1.0", + "react-dom": "19.2.4", + "react-hot-toast": "^2.6.0", + "react-icons": "^5.7.0", + "react-intersection-observer": "^10.1.0", + "react-loader-spinner": "^8.0.2", + "react-loading-indicators": "^1.0.1", + "react-loading-skeleton": "^3.5.0", + "react-markdown": "^10.1.0", + "react-scroll": "^1.9.3", + "react-select": "^5.10.2", + "react-share": "^5.3.0", + "react-social-media-embed": "^2.5.18", + "react-switch": "^7.1.0", + "resend": "^6.18.1", + "sass": "^1.102.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "zod": "^4.4.3" + }, + "overrides": { + "postcss": "^8.5.25", + "sharp": "^0.35.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.12", + "prisma": "^6.19.3", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/src/sections/Home/Hero/Hero.jsx b/src/sections/Home/Hero/Hero.jsx index 783898c..65482f1 100644 --- a/src/sections/Home/Hero/Hero.jsx +++ b/src/sections/Home/Hero/Hero.jsx @@ -56,7 +56,8 @@ function Hero() {
-

+ {/* Use a div instead of

so we don't place a block heading inside a paragraph */} +

Nurturing Using Innovative & Creative strategies{" "}

{currentTitle}

{" "} -

+
@@ -99,4 +100,4 @@ function Hero() { ); } -export default Hero; +export default Hero; \ No newline at end of file diff --git a/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx b/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx index 3b23a8f..8e16537 100644 --- a/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx +++ b/src/sections/Profile/Admin/Form/CertificatesForm/CertificatesForm.jsx @@ -7,7 +7,7 @@ import { Button } from "../../../../../components"; import { api } from "../../../../../services"; import { accessOrCreateEventByFormId, - // getCertificatePreview, + getCertificatePreview, generatedAndSendCertificate, } from "./tools/certificateTools"; import { Alert, MicroLoading } from "../../../../../microInteraction"; @@ -16,9 +16,11 @@ import AuthContext from "../../../../../context/AuthContext"; import Link from "next/link"; import { useParams } from "next/navigation"; -const CertificatesForm = () => { +const CertificatesForm = ({ eventId: propEventId } = {}) => { const authCtx = useContext(AuthContext); - const { eventId } = useParams(); + const params = useParams(); + const routeEventId = params?.id ?? params?.eventId ?? params?.formId; + const eventId = propEventId || routeEventId; const [certificate, setCertificate] = useState(null); const [certificateFile, setCertificateFile] = useState(null); const [fields, setFields] = useState([]); @@ -136,41 +138,35 @@ const CertificatesForm = () => { }; const handleRefresh = async () => { - if (!certificateFile) { + if (!eventId) { setAlert({ - type: "warning", - message: "Please upload a certificate image first", + type: "error", + message: "Event ID is missing. Please open this page from the certificate list.", position: "top-right", - duration: 3000, + duration: 4000, }); return; } + setPreviewLoading(true); try { - const formData = new FormData(); - formData.append("image", certificateFile); - formData.append("fields", JSON.stringify(fields)); - const response = await api.post( - "/api/certificate/dummyCertificate", - formData, - { - headers: { Authorization: `Bearer ${authCtx.token}` }, - } - ); - if (response.status !== 200) { - throw new Error(`API error: ${response.statusText}`); + const preview = await getCertificatePreview(eventId, authCtx.token); + if (!preview) { + throw new Error("No certificate template exists for this event yet."); } - setResponseImg(response.data.imageSrc); + + setResponseImg(preview); setAlert({ type: "success", - message: "Preview updated successfully", + message: "Preview loaded successfully", position: "top-right", duration: 2000, }); } catch (error) { setAlert({ type: "error", - message: "Error updating preview. Please try again", + message: + error?.message || "Error fetching preview. Please upload and save a template first.", position: "top-right", duration: 3000, }); @@ -180,6 +176,16 @@ const CertificatesForm = () => { }; const handleSave = async () => { + if (!eventId) { + setAlert({ + type: "error", + message: "Event ID is missing. Please open this page from the event certificate list.", + position: "top-right", + duration: 4000, + }); + return; + } + if (!certificateFile) { setAlert({ type: "warning", @@ -192,23 +198,9 @@ const CertificatesForm = () => { setSaveLoading(true); try { - const eventData = await accessOrCreateEventByFormId( - eventId, - authCtx.token - ); - if (!eventData || !eventData.id) { - throw new Error("Failed to retrieve or create event."); - } - - const formData = new FormData(); - formData.append("image", certificateFile); - formData.append("eventId", eventData.id); - formData.append("fields", JSON.stringify(fields)); - const response = await api.post( "/api/certificate/addCertificateTemplate", - - formData, + { eventId, template: certificate, fields }, { headers: { Authorization: `Bearer ${authCtx.token}` }, } @@ -228,7 +220,7 @@ const CertificatesForm = () => { console.error("Error saving certificate template:", error); setAlert({ type: "error", - message: "Error saving certificate template. Please try again", + message: error.response?.data?.message || "Error saving certificate template. Please try again", position: "top-right", duration: 3000, }); diff --git a/src/sections/Profile/Admin/Form/CertificatesForm/SendCertificate.jsx b/src/sections/Profile/Admin/Form/CertificatesForm/SendCertificate.jsx index 8845b37..82f00a9 100644 --- a/src/sections/Profile/Admin/Form/CertificatesForm/SendCertificate.jsx +++ b/src/sections/Profile/Admin/Form/CertificatesForm/SendCertificate.jsx @@ -27,9 +27,11 @@ const Checkbox = ({ id, checked, onCheckedChange }) => { ); }; -const SendCertificate = () => { +const SendCertificate = ({ eventId: propEventId } = {}) => { const authCtx = useContext(AuthContext); - const { eventId } = useParams(); + const params = useParams(); + const routeEventId = params?.id ?? params?.eventId ?? params?.formId; + const eventId = propEventId || routeEventId; const [loading, setLoading] = useState(false); const [previewLoading, setPreviewLoading] = useState(false); const [sendingMail, setSendingMail] = useState(false); @@ -42,23 +44,47 @@ const SendCertificate = () => { const [uncheckedFilterText, setUncheckedFilterText] = useState(""); const [checkedFilterText, setCheckedFilterText] = useState(""); const [fileUploading, setFileUploading] = useState(false); - const [certificatePreview, setCertificatePreview] = useState("Loading..."); + const [certificatePreview, setCertificatePreview] = useState(null); const [alert, setAlert] = useState(null); const [failedEmails, setFailedEmails] = useState([]); + const [deliveryError, setDeliveryError] = useState(""); const [isFailedMinimized, setIsFailedMinimized] = useState(false); useEffect(() => { const fetchCertificatePreview = async () => { + if (!eventId || eventId === "undefined" || eventId === "null") { + setCertificatePreview(null); + setAlert({ + type: "error", + message: "Event ID is missing from this link.", + position: "top-right", + duration: 4000, + }); + setPreviewLoading(false); + return; + } + setPreviewLoading(true); try { const preview = await getCertificatePreview(eventId, authCtx.token); if (preview) { setCertificatePreview(preview); + } else { + setCertificatePreview(null); + setAlert({ + type: "warning", + message: "No certificate template has been saved for this event yet.", + position: "top-right", + duration: 4000, + }); } } catch (error) { + setCertificatePreview(null); setAlert({ type: "error", - message: "Failed to load certificate preview", + message: + error.response?.data?.message || + "Failed to load certificate preview. Save a certificate template first.", position: "top-right", duration: 3000, }); @@ -205,6 +231,16 @@ const SendCertificate = () => { attendee.name.toLowerCase().includes(checkedFilterText.toLowerCase())) ); const handleSendBatchMail = async () => { + if (!eventId) { + setAlert({ + type: "error", + message: "Event ID is missing. Please open this page from the certificate list.", + position: "top-right", + duration: 4000, + }); + return; + } + if (!checkedAttendees.length) { setAlert({ type: "warning", @@ -217,37 +253,23 @@ const SendCertificate = () => { setSendingMail(true); setFailedEmails([]); + setDeliveryError(""); try { - const eventData = await accessOrCreateEventByFormId( - eventId, - authCtx.token - ); - if (!eventData || !eventData.id || !eventData.certificates?.length) { - throw new Error( - "Event data retrieval failed or certificates not found" - ); - } - const certificateId = - eventData.certificates[eventData.certificates.length - 1]?.id; - - if (!certificateId) { - throw new Error("Certificate ID not found"); + if (!eventId) { + throw new Error("Event ID is missing"); } const attendees = checkedAttendees.map((attendee) => ({ - fieldValues: { - name: attendee.name || "", - email: attendee.email, - }, - certificateId, + email: attendee.email, + name: attendee.name || "", })); if (attendees.length === 0) { throw new Error("No valid attendees found"); } const response = await generatedAndSendCertificate({ - eventId: eventData.id, + eventId, attendees, subject, body, @@ -274,10 +296,12 @@ const SendCertificate = () => { throw new Error(response?.data?.error || "Failed to send certificates"); } } catch (error) { + const message = error.response?.data?.message || error.message; + setDeliveryError(message); console.error("Error in handleSendBatchMail:", error); setAlert({ type: "error", - message: "Failed to send certificates: " + error.message, + message: "Failed to send certificates: " + message, position: "top-right", duration: 3000, }); @@ -287,6 +311,16 @@ const SendCertificate = () => { }; const handleTestMail = async () => { + if (!eventId) { + setAlert({ + type: "error", + message: "Event ID is missing. Please open this page from the certificate list.", + position: "top-right", + duration: 4000, + }); + return; + } + if (!checkedAttendees.length) { setAlert({ type: "warning", @@ -298,17 +332,14 @@ const SendCertificate = () => { } setSendingMail(true); + setDeliveryError(""); try { - const eventData = await accessOrCreateEventByFormId( - eventId, - authCtx.token - ); - if (!eventData || !eventData.id) { - throw new Error("Event data retrieval failed"); + if (!eventId) { + throw new Error("Event ID is missing"); } const response = await testCertificateSending({ - eventId: eventData.id, + eventId, email: checkedAttendees[0].email, name: checkedAttendees[0].name || "", subject: `[TEST] ${subject}`, @@ -323,12 +354,20 @@ const SendCertificate = () => { duration: 3000, }); } else { - throw new Error(response?.data?.error || "Failed to send test mail"); + throw new Error( + response?.data?.message || + response?.data?.error || + "Failed to send test mail" + ); } } catch (error) { + const message = error.response?.data?.message || error.message; + setDeliveryError(message); setAlert({ type: "error", - message: "Failed to send test mail: " + error.message, + message: + "Failed to send test mail: " + + message, position: "top-right", duration: 3000, }); @@ -374,7 +413,7 @@ const SendCertificate = () => { >
- ) : ( + ) : certificatePreview ? ( Certificate Preview { maxHeight: "270px", }} /> + ) : ( +

+ No certificate template found. Save one from Create Certificate first. +

)}
{ onChange={(e) => setMailFrequency(e.target.value)} style={{ marginTop: -10, width: "100%" }} /> + {deliveryError && ( +

+ {deliveryError} +

+ )}
{ try { - let res = await api.post( - "/api/certificate/getEventByFormId", - { formId }, - { - headers: { Authorization: `Bearer ${token}` }, - } - ); - - if (res.status !== 200) { - const form = await api.get("/api/form/getAllForms", { - params: { id: formId }, - headers: { Authorization: `Bearer ${token}` }, - }); - - if (form.status === 200) { - res = await api.post( - "/api/certificate/createOrganisationEvent", - { - name: form.data.events.info.eventTitle, - description: form.data.events.info.eventdescription, - organisationId: process.env.NEXT_PUBLIC_CERT_ORG, - formId: form.data.events.id, - }, - { - headers: { Authorization: `Bearer ${token}` }, - } - ); - } - } - - return res.data; + if (!formId) return null; + return { id: formId, certificates: [{ id: formId }] }; } catch (error) { - console.error("Error fetching event by form ID:", error); + console.error("Error resolving event id:", error); + return null; } }; const getCertificatePreview = async (formId, token) => { try { - const event = await accessOrCreateEventByFormId(formId, token); - const certificate = event.certificates[0].template; - const fields = event.certificates[0].fields; + if (!formId) { + console.warn("No event id was provided for certificate preview"); + return null; + } const cert = await api.post( "/api/certificate/dummyCertificate", { - imageLink: certificate, - fields, + eventId: formId, }, { headers: { Authorization: `Bearer ${token}` }, } ); - return cert.data.imageSrc; + return cert?.data?.template ?? cert?.data?.imageSrc ?? null; } catch (error) { console.error("Error fetching certificate preview:", error); + return null; } }; @@ -95,13 +71,22 @@ const generatedAndSendCertificate = async ({ token, }) => { try { + const recipients = (attendees ?? []).map((attendee) => ({ + email: attendee.email, + fieldValues: { + name: attendee.name || attendee.email, + email: attendee.email, + subject: subject || "Certificate of Appreciation", + body: body || "", + }, + })); + const response = await api.post( - "/api/certificate/sendCertViaEmail", + "/api/certificate/sendCertificatesAndEvents", { eventId, - attendees, - subject, - body, + recipients, + resend: true, }, { headers: { Authorization: `Bearer ${token}` }, @@ -116,7 +101,7 @@ const generatedAndSendCertificate = async ({ return response; } catch (error) { console.error("Failed to generate and send certificates:", error); - return error.response; + return error?.response ?? { status: 500, data: { message: "Failed to send certificates" } }; } }; @@ -136,7 +121,7 @@ const testCertificateSending = async ({ eventId, email, name, subject, token }) ); return response; } catch (error) { - console.error("Error sending test certificate:", error); + console.warn("Test certificate was not sent:", error.response?.data?.message); return error.response; } }; diff --git a/src/sections/Profile/Admin/View/CertificatePreview/CertificatePreview.jsx b/src/sections/Profile/Admin/View/CertificatePreview/CertificatePreview.jsx index ec8288f..74f5e4b 100644 --- a/src/sections/Profile/Admin/View/CertificatePreview/CertificatePreview.jsx +++ b/src/sections/Profile/Admin/View/CertificatePreview/CertificatePreview.jsx @@ -14,7 +14,9 @@ import { useRouter, useParams } from "next/navigation"; const CertificatesPreview = () => { const authCtx = useContext(AuthContext); - const { eventId, eventTitle } = useParams(); + const params = useParams(); + const eventId = params?.id ?? params?.eventId ?? params?.formId; + const eventTitle = params?.eventTitle ?? "Event Name"; const router = useRouter(); const [certificateData, setCertificateData] = useState({}); const [name, setName] = useState(""); diff --git a/src/sections/Profile/Admin/View/VerifyCertificate/VerifyCertificate.jsx b/src/sections/Profile/Admin/View/VerifyCertificate/VerifyCertificate.jsx index 2e591d7..5bc78d4 100644 --- a/src/sections/Profile/Admin/View/VerifyCertificate/VerifyCertificate.jsx +++ b/src/sections/Profile/Admin/View/VerifyCertificate/VerifyCertificate.jsx @@ -6,12 +6,13 @@ import { api } from "../../../../../services"; import { ComponentLoading } from "../../../../../microInteraction"; import styles from "./styles/VerifyCertificate.module.scss"; import { CheckCircle } from "lucide-react"; +import { QRCodeSVG } from "qrcode.react"; import Share from "../../../../../features/Modals/Event/ShareModal/ShareModal"; import shareOutline from "../../../../../assets/images/shareOutline.svg"; import { useParams, useSearchParams } from "next/navigation"; const VerifyCertificate = () => { - const [searchParams] = useSearchParams(); + const searchParams = useSearchParams(); const certificateId = searchParams.get("id"); const { issuedCertificateId } = useParams(); const [certificateData, setCertificateData] = useState(null); @@ -48,6 +49,8 @@ const VerifyCertificate = () => { email: response.data.certificate.email || "N/A", event: response.data.event?.name || "N/A", date: response.data.event?.createdAt || "N/A", + fields: response.data.certificate.fields || response.data.template?.fields || [], + fieldValues: response.data.certificate.fieldValues || {}, }); } else { setError("Invalid certificate data."); @@ -74,6 +77,31 @@ const VerifyCertificate = () => { } }; + const certificateFields = (certificateData?.fields || []) + .map((field) => { + const fieldName = String(field.fieldName || "").trim(); + const valueKey = Object.keys(certificateData?.fieldValues || {}).find( + (key) => key.toLowerCase() === fieldName.toLowerCase() + ); + const value = valueKey ? certificateData.fieldValues[valueKey] : ""; + return { ...field, value }; + }) + .filter((field) => field.fieldName && field.value); + + // Older templates may not have editable field coordinates. Keep these + // certificates readable by placing the recipient name in the intended + // centre area until the template is edited with a named field. + if (!certificateFields.length && certificateData?.name && certificateData.name !== "N/A") { + certificateFields.push({ + fieldName: "name", + value: certificateData.name, + x: 50, + y: 53, + fontSize: 22, + fontColor: "#1c1c1c", + }); + } + const copyLink = () => { navigator.clipboard.writeText(currentUrl).then(() => { setCopied(true); @@ -110,7 +138,34 @@ const VerifyCertificate = () => {
- Verified Certificate +
+ Verified Certificate + {certificateFields.map((field, index) => ( + + {String(field.value)} + + ))} +
+ +
+
diff --git a/src/sections/Profile/Admin/View/VerifyCertificate/styles/VerifyCertificate.module.scss b/src/sections/Profile/Admin/View/VerifyCertificate/styles/VerifyCertificate.module.scss index aeb6847..5f0dc7d 100644 --- a/src/sections/Profile/Admin/View/VerifyCertificate/styles/VerifyCertificate.module.scss +++ b/src/sections/Profile/Admin/View/VerifyCertificate/styles/VerifyCertificate.module.scss @@ -50,6 +50,41 @@ $shadow: 0 8px 24px rgba(0, 0, 0, 0.2); justify-content: center; align-items: center; + .certificateCanvas { + position: relative; + width: 95%; + line-height: 0; + + img { + display: block; + width: 100%; + max-width: 100%; + } + } + + .certificateField { + position: absolute; + z-index: 1; + transform: translate(-50%, -50%); + max-width: 85%; + overflow-wrap: anywhere; + font-weight: 600; + line-height: 1.2; + pointer-events: none; + text-align: center; + } + + .qrCode { + position: absolute; + right: 2.5%; + bottom: 4%; + z-index: 2; + display: flex; + padding: 3px; + background: #fff; + border: 1px solid #1c1c1c; + } + img { max-width: 95%; border-radius: $border-radius; @@ -209,4 +244,4 @@ $shadow: 0 8px 24px rgba(0, 0, 0, 0.2); background-color: darken($accent-color, 10%); transform: scale(1.05); } -} \ No newline at end of file +} diff --git a/src/sections/Profile/General/CertificatesView/CertificatesView.jsx b/src/sections/Profile/General/CertificatesView/CertificatesView.jsx index 2c8ae08..c5632ff 100644 --- a/src/sections/Profile/General/CertificatesView/CertificatesView.jsx +++ b/src/sections/Profile/General/CertificatesView/CertificatesView.jsx @@ -6,20 +6,18 @@ import AuthContext from "../../../../context/AuthContext"; import { api } from "../../../../services"; import { ComponentLoading } from "../../../../microInteraction"; -import { Send } from "lucide-react"; import Link from "next/link"; const Events = () => { const authCtx = useContext(AuthContext); - const [events, setEvents] = useState([]); + const [events, setEvents] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const viewPath = "/profile/Events"; + const [error, setError] = useState(null); + const [certCounts, setCertCounts] = useState>({}); + const SendCertificatePath = "/profile/events/SendCertificate"; - const analyticsPath = "/profile/events/Analytics"; const createCertificatesPath = "/profile/events/createCertificates"; const viewCertificatesPath = "/profile/events/viewCertificates"; - const analyticsAccessRoles = [ "PRESIDENT", @@ -34,70 +32,133 @@ const Events = () => { useEffect(() => { const fetchEventsData = async () => { + setIsLoading(true); try { const response = await api.get("/api/form/getAllForms"); - const userEvents = authCtx.user.regForm; + const userEvents = authCtx?.user?.regForm ?? []; if (response.status === 200) { - let fetchedEvents = response.data.events; - if (authCtx.user.access !== "USER") { - // Set events for non-users + let fetchedEvents = response.data.events ?? []; + if (authCtx?.user?.access !== "USER") { + // non-user (admin etc.) see all events setEvents(sortEventsByDate(fetchedEvents)); } else { - // Filter and then sort events for users - const filteredEvents = fetchedEvents.filter((event) => - userEvents.includes(event.id) + // users see only registered events + const filteredEvents = fetchedEvents.filter((event: any) => + userEvents.includes(event.id ?? event._id) ); setEvents(sortEventsByDate(filteredEvents)); } } else { - console.error("Error fetching event data:", response.data.message); + console.error("Error fetching event data:", response.data?.message); setError({ message: "Sorry for the inconvenience, we are having issues fetching your Events", }); } - } catch (error) { + } catch (err) { + console.error("Error fetching events:", err); setError({ message: "Sorry for the inconvenience, we are having issues fetching your Events", }); - console.error("Error fetching team members:", error); - - // const userEvents = authCtx.user.regForm; - // // using local JSON data - // let localEvents = eventsData.events; - // if (authCtx.user.access !== "USER") { - // setEvents(sortEventsByDate(localEvents)); - // } else { - // const filteredEvents = localEvents.filter((event) => - userEvents.includes(event._id) - // ); - // setEvents(sortEventsByDate(filteredEvents)); - // } } finally { setIsLoading(false); } }; - fetchEventsData(); - }, [authCtx.user.email]); + // only fetch if authCtx.user exists + if (authCtx?.user) { + fetchEventsData(); + } else { + setIsLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authCtx?.user?.email, authCtx?.user]); + + useEffect(() => { + // fetch certificate counts for shown events + const loadCounts = async () => { + if (!events || events.length === 0) { + setCertCounts({}); + return; + } + + const promises = events.map(async (ev) => { + const id = ev.id ?? ev._id; + if (!id) return { id: null, count: 0 }; + + try { + // Try to fetch a list of certificates for the event and use length as count. + // Adjust endpoint if your backend has a dedicated count endpoint. + const resp = await api.get(`/api/certificates/${id}`); + // resp.data may be { files: [...] } or an array; be defensive: + const data = resp.data; + let count = 0; + if (Array.isArray(data)) count = data.length; + else if (Array.isArray(data.files)) count = data.files.length; + else if (Array.isArray(data.certificates)) count = data.certificates.length; + else if (typeof data.count === "number") count = data.count; + else count = 0; + return { id, count }; + } catch (e) { + // If endpoint fails, fallback to 0 + return { id, count: 0 }; + } + }); + + const results = await Promise.all(promises); + const map: Record = {}; + results.forEach((r) => { + if (r.id) map[r.id] = r.count; + }); + setCertCounts(map); + }; + + loadCounts(); + }, [events]); + + const sortEventsByDate = (evts: any[]) => { + // copy before sorting to avoid mutating original array + return [...(evts ?? [])].sort( + (a, b) => + new Date(b.info?.eventDate ?? 0).getTime() - + new Date(a.info?.eventDate ?? 0).getTime() + ); + }; + + const getEventId = (event) => { + const candidates = [ + event?.id, + event?._id, + event?.formId, + event?.eventId, + event?.info?.id, + event?.info?._id, + event?.info?.formId, + event?.info?.eventId, + event?.extra?.id, + event?.extra?._id, + event?.extra?.formId, + event?.extra?.eventId, + ]; - const sortEventsByDate = (events) => { - return events.sort((a, b) => new Date(b.info.eventDate) - new Date(a.info.eventDate)); + return candidates.find( + (value) => value !== undefined && value !== null && value !== "" && value !== "undefined" && value !== "null" + ); }; - const formatDate = (dateString) => { - const options = { day: "2-digit", month: "2-digit", year: "numeric" }; + const formatDate = (dateString: string) => { + if (!dateString) return "-"; + const options = { day: "2-digit", month: "2-digit", year: "numeric" } as const; return new Date(dateString) .toLocaleDateString("en-GB", options) .replace(/\//g, "-"); }; - // console.log("Event Access",authCtx.user.access); return (
- {authCtx.user.access !== "USER" ? ( + {authCtx?.user?.access !== "USER" ? (

Events Timeline @@ -125,61 +186,41 @@ const Events = () => { Event Name Event Date Certificates - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( - <> - Manage Mail - Create/Edit + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + <> + Manage Mail + Create/Edit )} - {/* Add more headers */} - {events.map((event) => ( - - - {event.info.eventTitle} - - - {formatDate(event.info.eventDate)} - - - - - - - - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( - - - - + {events.map((event) => { + const id = getEventId(event)?.toString(); + if (!id) { + console.warn("Certificate list event missing id:", event); + return null; + } + + const certCount = certCounts[id] ?? 0; + + return ( + + + {event.info?.eventTitle ?? "Untitled Event"} + + + + {formatDate(event.info?.eventDate)} - )} - {(analyticsAccessRoles.includes(authCtx.user.access) || authCtx.user.email == "srex@fedkiit.com") && ( + - + - )} - - - ))} + + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + + + + + + )} + + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + + + + + + )} + + ); + })} ) : ( @@ -209,4 +293,4 @@ const Events = () => { ); }; -export default Events; +export default Events; \ No newline at end of file diff --git a/src/sections/Profile/General/CertificatesView/CertificatesView.tsx b/src/sections/Profile/General/CertificatesView/CertificatesView.tsx new file mode 100644 index 0000000..d5ffe9f --- /dev/null +++ b/src/sections/Profile/General/CertificatesView/CertificatesView.tsx @@ -0,0 +1,271 @@ +"use client"; + +import { useContext, useEffect, useState } from "react"; +import styles from "./styles/CertificatesView.module.scss"; +import AuthContext from "../../../../context/AuthContext"; + +import { api } from "../../../../services"; +import { ComponentLoading } from "../../../../microInteraction"; +import Link from "next/link"; + +const Events = () => { + const authCtx = useContext(AuthContext); + const [events, setEvents] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [certCounts, setCertCounts] = useState>({}); + + const SendCertificatePath = "/profile/events/SendCertificate"; + const createCertificatesPath = "/profile/events/createCertificates"; + const viewCertificatesPath = "/profile/events/viewCertificates"; + + const analyticsAccessRoles = [ + "PRESIDENT", + "VICEPRESIDENT", + "DIRECTOR_CREATIVE", + "DIRECTOR_TECHNICAL", + "DIRECTOR_MARKETING", + "DIRECTOR_OPERATIONS", + "DIRECTOR_SPONSORSHIP", + "ADMIN", + ]; + + useEffect(() => { + const fetchEventsData = async () => { + setIsLoading(true); + try { + const response = await api.get("/api/form/getAllForms"); + const userEvents = authCtx?.user?.regForm ?? []; + + if (response.status === 200) { + let fetchedEvents = response.data.events ?? []; + if (authCtx?.user?.access !== "USER") { + // non-user (admin etc.) see all events + setEvents(sortEventsByDate(fetchedEvents)); + } else { + // users see only registered events + const filteredEvents = fetchedEvents.filter((event: any) => + userEvents.includes(event.id ?? event._id) + ); + setEvents(sortEventsByDate(filteredEvents)); + } + } else { + console.error("Error fetching event data:", response.data?.message); + setError({ + message: + "Sorry for the inconvenience, we are having issues fetching your Events", + }); + } + } catch (err) { + console.error("Error fetching events:", err); + setError({ + message: + "Sorry for the inconvenience, we are having issues fetching your Events", + }); + } finally { + setIsLoading(false); + } + }; + + // only fetch if authCtx.user exists + if (authCtx?.user) { + fetchEventsData(); + } else { + setIsLoading(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authCtx?.user?.email, authCtx?.user]); + + useEffect(() => { + // fetch certificate counts for shown events + const loadCounts = async () => { + if (!events || events.length === 0) { + setCertCounts({}); + return; + } + + const promises = events.map(async (ev) => { + const id = ev.id ?? ev._id; + if (!id) return { id: null, count: 0 }; + + try { + // Try to fetch a list of certificates for the event and use length as count. + // Adjust endpoint if your backend has a dedicated count endpoint. + + const resp = await api.get(`/api/certificates/${id}`); + // resp.data may be { files: [...] } or an array; be defensive: + const data = resp.data; + let count = 0; + if (Array.isArray(data)) count = data.length; + else if (Array.isArray(data.files)) count = data.files.length; + else if (Array.isArray(data.certificates)) count = data.certificates.length; + else if (typeof data.count === "number") count = data.count; + else count = 0; + return { id, count }; + } catch (e) { + // If endpoint fails, fallback to 0 + return { id, count: 0 }; + } + }); + + const results = await Promise.all(promises); + const map: Record = {}; + results.forEach((r) => { + if (r.id) map[r.id] = r.count; + }); + setCertCounts(map); + }; + + loadCounts(); + }, [events]); + + const sortEventsByDate = (evts: any[]) => { + // copy before sorting to avoid mutating original array + return [...(evts ?? [])].sort( + (a, b) => + new Date(b.info?.eventDate ?? 0).getTime() - + new Date(a.info?.eventDate ?? 0).getTime() + ); + }; + + const formatDate = (dateString: string) => { + if (!dateString) return "-"; + const options = { day: "2-digit", month: "2-digit", year: "numeric" } as const; + return new Date(dateString) + .toLocaleDateString("en-GB", options) + .replace(/\//g, "-"); + }; + + return ( +
+ {authCtx?.user?.access !== "USER" ? ( +
+

+ Events Timeline +

+
+ ) : ( +
+

+ Participated Events +

+
+ )} + + {isLoading ? ( + + ) : ( + <> + {error &&
{error.message}
} + +
+ {events.length > 0 ? ( + + + + + + + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + <> + + + + )} + + + + + {events.map((event) => { + const id = event.id ?? event._id ?? event._id?.toString(); + const certCount = certCounts[id] ?? 0; + + return ( + + + + + + + + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + + )} + + {(analyticsAccessRoles.includes(authCtx?.user?.access) || + authCtx?.user?.email === "srex@fedkiit.com") && ( + + )} + + ); + })} + +
Event NameEvent DateCertificatesManage MailCreate/Edit
+ {event.info?.eventTitle ?? "Untitled Event"} + + {formatDate(event.info?.eventDate)} + + + + + + + + + + + + +
+ ) : ( +

Not participated in any Events

+ )} +
+ + )} +
+ ); +}; + +export default Events; \ No newline at end of file diff --git a/theirs_layout.jsx b/theirs_layout.jsx new file mode 100644 index 0000000..14a9621 Binary files /dev/null and b/theirs_layout.jsx differ