From 22fe46ed6505e9ed06d020372ca35d754dce1fc9 Mon Sep 17 00:00:00 2001 From: Justin McLellan Date: Wed, 16 Sep 2026 22:33:33 -0500 Subject: [PATCH 1/2] feat: migrate database queries to Prisma --- __tests__/prisma-client.test.ts | 10 + lib/auth.ts | 40 +-- lib/db.ts | 40 --- lib/prisma.ts | 27 ++ next.config.js | 11 - .../.openspec.yaml | 3 + .../design.md | 86 +++++++ .../inventory.md | 27 ++ .../proposal.md | 32 +++ .../raw-query-exceptions.md | 11 + .../tasks.md | 48 ++++ package.json | 4 +- pages/api/addassignment.ts | 39 +-- pages/api/addremark.ts | 34 +-- pages/api/addstudenttobatch.ts | 120 ++++----- pages/api/audit/create.ts | 27 +- pages/api/audit/get.ts | 91 +++---- pages/api/batchcreate.ts | 64 ++--- pages/api/batches.ts | 7 +- pages/api/bulkregcheckstudentduplicates.ts | 43 +--- pages/api/bulkstudentapplication.ts | 116 ++++----- pages/api/countbatch.ts | 15 +- pages/api/countbatchesmonth.ts | 19 +- pages/api/countcourse.ts | 24 +- pages/api/countcoursesmonth.ts | 19 +- pages/api/countenrollsmonth.ts | 19 +- pages/api/countinstructorsmonth.ts | 19 +- pages/api/countleadsmonth.ts | 19 +- pages/api/countstudent.ts | 16 +- pages/api/counttasmonth.ts | 19 +- pages/api/coursecreate.ts | 40 ++- pages/api/deleteassignment.ts | 7 +- pages/api/deletebatch.ts | 15 +- pages/api/deletecourse.ts | 19 +- pages/api/deletestudent.ts | 12 +- pages/api/deletestudentfrombatch.ts | 79 ++---- pages/api/deleteuser.ts | 43 ++-- pages/api/dropdownDataPm.ts | 10 +- pages/api/dropdownDataStaff.ts | 6 +- pages/api/getBatchesWithEnrollCount.ts | 74 +++--- pages/api/getCoursesWithEnrollCount.ts | 26 +- pages/api/getYearlyEnrollmentTrend.ts | 14 +- pages/api/getbatchdetails.ts | 73 +++--- pages/api/getbatchesdata.ts | 13 +- pages/api/getbatchesreport.ts | 16 +- pages/api/getcoursesdata-test.ts | 22 +- pages/api/getcoursesdata.ts | 17 +- pages/api/getdocumentsfee.ts | 74 +++--- pages/api/getstudent.ts | 38 ++- pages/api/getstudentdetails.ts | 97 +++---- pages/api/getstudentsdata.ts | 25 +- pages/api/getunassignedstudents.ts | 22 +- pages/api/getuserdata.ts | 17 +- pages/api/getusersdata.ts | 42 +-- pages/api/patchattendance.ts | 72 +++--- pages/api/studentapplication.ts | 97 ++++--- pages/api/telecaller-remarks/[id].ts | 37 ++- pages/api/telecaller-remarks/index.ts | 51 ++-- pages/api/updateassignment.ts | 23 +- pages/api/updatebatchattendance.ts | 8 +- pages/api/updatebatches.ts | 120 +++++---- pages/api/updatebatchstatus.ts | 38 ++- pages/api/updatecourse.ts | 51 +++- pages/api/updatedocumentsfee.ts | 32 +-- pages/api/updategrade.ts | 18 +- pages/api/updatestudents.ts | 29 +-- pages/api/updateusers.ts | 118 +++------ pages/api/usercreate.ts | 74 +++--- pages/api/users.ts | 7 +- pages/api/va-remarks/[id].ts | 33 ++- pages/api/va-remarks/index.ts | 49 ++-- pnpm-lock.yaml | 90 ++++--- utils/batch-status-rules-config.ts | 41 +-- utils/dropdown-config.ts | 243 +++++++----------- 74 files changed, 1464 insertions(+), 1617 deletions(-) create mode 100644 __tests__/prisma-client.test.ts delete mode 100644 lib/db.ts create mode 100644 lib/prisma.ts create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/.openspec.yaml create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/design.md create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/inventory.md create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/proposal.md create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/raw-query-exceptions.md create mode 100644 openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/tasks.md diff --git a/__tests__/prisma-client.test.ts b/__tests__/prisma-client.test.ts new file mode 100644 index 00000000..bc73ead2 --- /dev/null +++ b/__tests__/prisma-client.test.ts @@ -0,0 +1,10 @@ +import { prisma } from "@/lib/prisma"; + +describe("shared Prisma client", () => { + it("exposes generated model delegates through the shared client", () => { + expect(prisma).toBeDefined(); + expect(prisma.vausers).toBeDefined(); + expect(prisma.vastudents).toBeDefined(); + expect(prisma.vabatches).toBeDefined(); + }); +}); diff --git a/lib/auth.ts b/lib/auth.ts index 6e3cc742..a453b4fc 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -1,39 +1,21 @@ import { passkey } from "@better-auth/passkey"; +import { prismaAdapter } from "@better-auth/prisma-adapter"; +import type { Prisma } from "@prisma/client"; import { betterAuth } from "better-auth"; import { APIError } from "better-auth/api"; import { customSession } from "better-auth/plugins"; -import { createPool } from "mysql2/promise"; +import { prisma } from "./prisma"; import { sendAuthEmail } from "./auth-email"; -const database = createPool({ - host: process.env.MYSQL_HOST, - port: Number(process.env.MYSQL_PORT ?? 3306), - database: process.env.MYSQL_DATABASE, - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, - timezone: "Z", - connectionLimit: 10, -}); - -type VaUser = { - id: number; - name: string | null; - role: string | null; -}; +type VaUser = Prisma.vausersGetPayload<{ + select: { id: true; name: true; role: true }; +}>; async function findVaUser(email: string): Promise { - const [rows] = await database.execute("SELECT id, name, role FROM vausers WHERE LOWER(email) = LOWER(?) LIMIT 1", [ - email, - ]); - - if (!Array.isArray(rows) || rows.length === 0) return null; - const row = rows[0]; - if (!row || typeof row !== "object" || !("id" in row)) return null; - return { - id: Number(row.id), - name: typeof row.name === "string" ? row.name : null, - role: typeof row.role === "string" ? row.role : null, - }; + return prisma.vausers.findFirst({ + where: { email }, + select: { id: true, name: true, role: true }, + }); } const baseURL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_BASE_URL; @@ -43,7 +25,7 @@ export const auth = betterAuth({ appName: "Vision-Aid STATS", baseURL, secret: process.env.BETTER_AUTH_SECRET, - database, + database: prismaAdapter(prisma, { provider: "mysql", transaction: true }), session: { expiresIn: 60 * 60 * 24 * 7, updateAge: 60 * 60, diff --git a/lib/db.ts b/lib/db.ts deleted file mode 100644 index b4c01120..00000000 --- a/lib/db.ts +++ /dev/null @@ -1,40 +0,0 @@ -import mysql from "mysql2/promise"; - -export type QueryValues = readonly unknown[] | undefined; -export type QueryRow = Record; -export type QueryMutationResult = { - affectedRows?: number; - insertId?: number; -}; -export type QueryResult = QueryRow[] & QueryMutationResult; - -export async function executeQuery({ query, values }: { query: string; values?: QueryValues }): Promise { - const db = await mysql.createConnection({ - host: process.env.MYSQL_HOST, - port: Number(process.env.MYSQL_PORT ?? 3306), - database: process.env.MYSQL_DATABASE, - user: process.env.MYSQL_USER, - password: process.env.MYSQL_PASSWORD, - }); - - try { - const [results] = await db.execute(query, values); - if (Array.isArray(results)) { - return results.map((row) => - row && typeof row === "object" && !Array.isArray(row) ? Object.fromEntries(Object.entries(row)) : {} - ); - } - const mutationResult: QueryRow[] = []; - if (results && typeof results === "object") { - Object.assign(mutationResult, { - affectedRows: "affectedRows" in results && typeof results.affectedRows === "number" ? results.affectedRows : 0, - insertId: "insertId" in results && typeof results.insertId === "number" ? results.insertId : 0, - }); - } - return mutationResult; - } catch (error) { - throw Error(error instanceof Error ? error.message : String(error)); - } finally { - await db.end(); - } -} diff --git a/lib/prisma.ts b/lib/prisma.ts new file mode 100644 index 00000000..4da01a46 --- /dev/null +++ b/lib/prisma.ts @@ -0,0 +1,27 @@ +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; +import { PrismaClient } from "@prisma/client"; + +declare global { + // eslint-disable-next-line no-unused-vars + var prisma: PrismaClient | undefined; +} + +const adapter = new PrismaMariaDb({ + host: process.env.MYSQL_HOST ?? "localhost", + port: Number(process.env.MYSQL_PORT ?? 3306), + user: process.env.MYSQL_USER ?? "", + password: process.env.MYSQL_PASSWORD ?? "", + database: process.env.MYSQL_DATABASE ?? "", + connectionLimit: 10, +}); + +export const prisma = + globalThis.prisma ?? + new PrismaClient({ + adapter, + log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"], + }); + +if (process.env.NODE_ENV !== "production") { + globalThis.prisma = prisma; +} diff --git a/next.config.js b/next.config.js index 0d0a76f0..e8401d14 100644 --- a/next.config.js +++ b/next.config.js @@ -9,17 +9,6 @@ module.exports = withPWA({ output: "standalone", pageExtensions: ["mdx", "md", "tsx", "ts", "svg"], - /** - * mysql2 uses dynamic requires; bundling it for API routes can fail with - * `Cannot find module './chunks/undefined'`. Load it from node_modules at runtime. - */ - webpack: (config, { isServer }) => { - if (isServer) { - config.externals.push("mysql2", "mysql2/promise"); - } - return config; - }, - async headers() { return [ { diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/.openspec.yaml b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/.openspec.yaml new file mode 100644 index 00000000..454f4e06 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/.openspec.yaml @@ -0,0 +1,3 @@ +schema: spec-driven +created: 2026-09-16 +skip_specs: true diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/design.md b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/design.md new file mode 100644 index 00000000..98f95a82 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/design.md @@ -0,0 +1,86 @@ +## Context + +The Prisma schema is split across `prisma/models` and `prisma/enums` and already models the primary application tables and relations. The application has no shared Prisma client yet. Instead, 57 TypeScript API/utilities import `lib/db.ts`, which opens a MySQL connection per call and exposes rows as `Record`; `lib/auth.ts` also owns a separate MySQL pool for a custom `vausers` lookup and passes that pool to Better Auth. + +The migration must preserve Pages Router endpoints, existing JSON shapes, legacy column names and enum values, MySQL behavior, and the Better Auth integration. Some current SQL contains joins, aggregates, dynamic filters, upserts, or vendor-specific expressions that may require a deliberately typed raw-query exception rather than a semantic rewrite. + +## Goals / Non-Goals + +**Goals:** + +- Establish one shared server-side Prisma Client entry point for application-owned database access. +- Replace generic query/mutation result handling with generated model, enum, relation, and input types. +- Preserve existing API behavior, response keys, error behavior, transaction boundaries, and database semantics. +- Make raw SQL exceptional, parameterized, locally typed, and easy to audit. +- Verify the migration with compiler, test, build, and representative database-backed route checks. + +**Non-Goals:** + +- Redesigning endpoint payloads, route structure, UI behavior, authentication policy, or database schema. +- Replacing Better Auth's internal database adapter unless the installed Better Auth version provides a compatible Prisma adapter and the migration can preserve its required schema and behavior. +- Converting reporting queries to a new data model or optimizing queries beyond what is required to preserve behavior. +- Adding a broad strict-mode or unrelated type-safety initiative. + +## Decisions + +### Use a singleton Prisma client module + +Create a server-only module such as `lib/prisma.ts` that exports a single `PrismaClient`, with development global reuse to avoid exhausting connections during hot reload and normal production singleton behavior. The module will be the only normal import boundary for `@prisma/client` in application code. + +Alternative considered: instantiate `PrismaClient` in every route. This would duplicate lifecycle behavior and can create too many connections during development and serverless execution. + +### Migrate by domain and preserve route response mapping + +Group routes by domain—configuration, courses/batches, students/enrollment, attendance/grades/fees, users/auth, remarks, audit, and reporting. Replace each SQL statement with a typed Prisma operation, then map the selected records to the existing response shape at the route boundary where SQL aliases or date formatting were previously used. Prefer explicit `select` objects over `include` when the endpoint only needs a subset. + +Alternative considered: expose Prisma records directly from every route. That would leak schema naming and Date/BigInt serialization details into the public API and would change response contracts. + +### Prefer relations and typed aggregate operations + +Use Prisma relations for joins and nested writes, `where`/`orderBy`/`select` for filtering and projection, `count`/`aggregate`/`groupBy` for supported metrics, and `$transaction` for multi-step mutations that currently depend on ordered statements. Use generated enum values rather than string literals for modeled enums. + +Alternative considered: retain SQL strings behind a new wrapper. That would preserve the weak result typing and would not deliver the requested Prisma type safety. + +### Constrain raw SQL to a typed exception path + +When Prisma cannot express a MySQL-specific query without changing behavior, use `$queryRaw`/`$executeRaw` with Prisma template-tag parameterization. Define a local result type for each raw query and keep the query in the owning domain module. Dynamic identifiers must come from an allowlisted mapping, never interpolated request input. Each exception will be documented with why the typed model API is insufficient. + +Alternative considered: force every query through Prisma model APIs. This risks changing report totals, date/string semantics, regex filtering, or upsert behavior in legacy MySQL queries. + +### Keep data normalization explicit at external boundaries + +Use generated Prisma result types internally, then explicitly normalize values needed by JSON responses: dates, BigInt fields, nullable legacy columns, counts, and enum strings. Request bodies and query parameters remain runtime-validated before being used in Prisma `where` or `data` objects. No generic `QueryRow`, `QueryResult`, or unchecked cast-based escape hatch will replace the old helper. + +Alternative considered: serialize Prisma results generically. That hides nullable/BigInt/date issues and weakens the type contract at exactly the route boundary being improved. + +### Separate application queries from Better Auth adapter ownership + +Move `findVaUser` to Prisma because it is an application-owned query. Keep Better Auth's database option on its supported adapter unless a compatible Prisma adapter is available for the installed version; if one is available, evaluate it as a focused subtask and verify the Better Auth tables and lifecycle hooks before changing it. This avoids pretending that an adapter swap is safe while still removing the hand-written custom MySQL lookup. + +Alternative considered: replace Better Auth's adapter without compatibility verification. That could break session, account, passkey, verification, or transaction behavior even if application routes compile. + +## Risks / Trade-offs + +- [Risk] Prisma model field types or relations do not fully match legacy columns and nullability. → Compare every migrated statement with the schema and migration SQL; correct only clearly missing Prisma mappings and require explicit review for schema-affecting changes. +- [Risk] Date, BigInt, enum, alias, or count serialization changes API responses. → Preserve existing response mappers and add fixture-level assertions for representative endpoints. +- [Risk] Converting multi-statement mutations changes atomicity or ordering. → Use `$transaction` only where the existing operation is logically a unit, and test partial-failure behavior before and after migration. +- [Risk] Raw SQL exceptions preserve behavior but reduce type safety. → Parameterize them, define exact result types, allowlist identifiers, document each exception, and track them for future reduction. +- [Risk] A shared client or Better Auth adapter change causes connection leaks or authentication regressions. → Add lifecycle tests/checks, keep adapter changes isolated, and run sign-in, session, password, and passkey smoke checks. +- [Risk] The large route count makes incomplete migration easy to miss. → Use repository-wide searches for `lib/db`, `mysql2`, `executeQuery`, and unapproved SQL after each domain batch and make type-check/build gates mandatory. + +## Migration Plan + +1. Inventory every `executeQuery` call, direct MySQL query, response shape, transaction-like sequence, and Prisma schema mismatch; capture current type-check, test, and build baselines. +2. Add the shared Prisma client module and test its initialization/lifecycle behavior without changing route code. +3. Migrate low-risk configuration, course, batch, user, and remark endpoints in domain batches, adding typed selects, mutations, relation queries, and response mappers. +4. Migrate student enrollment, attendance, grades, fees, bulk operations, and multi-statement mutations with explicit transaction decisions. +5. Migrate reports, dashboard counts, audit queries, dropdown utilities, and dynamic-filter paths; document any remaining raw SQL exception. +6. Migrate the custom authentication `vausers` lookup to Prisma and separately verify whether Better Auth's adapter should remain MySQL-backed or can safely use Prisma. +7. Remove `lib/db.ts` and its exported generic query types once repository searches show no callers; remove `mysql2` only if no supported Better Auth or other runtime integration still requires it. +8. Run formatting, lint, type-check, Jest, production build, Prisma validation/generation, and targeted API/auth/database smoke checks. Compare representative response fixtures and mutation results against the baseline. + +Rollback is a source/deployment rollback to the last known-good commit. No data migration is planned; if a Prisma mapping requires a database migration, pause that route batch and review it separately rather than applying an implicit schema change. + +## Open Questions + +- Whether the installed Better Auth release exposes a supported Prisma adapter compatible with the existing `user`, `account`, `session`, `verification`, and `passkey` models. This can be resolved during the auth batch without changing the overall application-query migration approach. diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/inventory.md b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/inventory.md new file mode 100644 index 00000000..06d1c847 --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/inventory.md @@ -0,0 +1,27 @@ +# Prisma migration inventory + +Captured on the `feature/switch-db-queries-to-prisma` branch. + +## Current database access + +- 57 TypeScript files import `@/lib/db` and call `executeQuery`. +- `lib/db.ts` creates and closes a `mysql2/promise` connection for each call. +- `lib/auth.ts` owns a separate `mysql2/promise` pool for the custom `vausers` lookup and Better Auth database adapter. +- Prisma 7.10.0 is installed, the schema is valid, and the repository currently has no shared application Prisma client module. + +## Domains + +- Configuration: `utils/dropdown-config.ts`, `utils/batch-status-rules-config.ts` +- Courses/batches: course and batch CRUD, batch details/reports, enrollment counts/trends +- Users/auth: user CRUD/dropdowns/counts, `lib/auth.ts` +- Students/enrollment: student CRUD, registration, duplicate checks, student/batch membership +- Attendance/grades/fees: attendance, assignments, grades, fee/document updates +- Remarks/audit: VA remarks, telecaller remarks, audit log routes +- Metrics: monthly count routes and dashboard/report data + +## Baseline commands + +- `pnpm type-check`: passed +- `pnpm prisma validate`: passed +- `pnpm test -- --runInBand`: pre-existing failure; no tests found +- `pnpm build`: compiled successfully and generated all static pages; the command did not return cleanly in the current tool session and needs repeat verification after implementation. diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/proposal.md b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/proposal.md new file mode 100644 index 00000000..661650aa --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/proposal.md @@ -0,0 +1,32 @@ +## Why + +The application has a Prisma schema and generated client available, but most API routes and utilities still issue untyped SQL through `lib/db.ts`, while authentication maintains a separate direct MySQL query path. This creates weak `Record` boundaries, duplicates connection handling, and prevents Prisma model types, relations, enums, and checked input types from protecting database-backed code. + +## What Changes + +- Replace all application queries currently routed through `lib/db.ts` with Prisma Client model operations and generated Prisma types. +- Migrate the direct `mysql2` query in `lib/auth.ts` to the shared Prisma client so all application-owned reads and writes use one typed database access layer. +- Add a server-only, reusable Prisma client module with the repository’s expected development and production lifecycle behavior. +- Convert SQL joins, filters, ordering, aggregates, mutations, and transactions to typed Prisma queries, preserving existing endpoint response shapes and runtime behavior. +- Use Prisma relations, `select`/`include`, enums, generated input types, and explicit result types at API and utility boundaries; eliminate generic query-row/mutation-result typing from application code. +- Use parameterized Prisma raw SQL only for operations that cannot be represented without changing behavior, with a documented and narrowly typed exception policy. +- Remove `lib/db.ts`, its MySQL connection-per-query implementation, and any now-unused `mysql2` application dependency after all callers are migrated. +- Add or update tests and validation so type-checking fails when query results or mutation inputs bypass Prisma-generated types. + +## Capabilities + +### New Capabilities + +None. This is a behavior-preserving data-access refactor and type-safety improvement. + +### Modified Capabilities + +None. Existing routes, response payloads, authentication behavior, and database semantics remain unchanged. + +## Impact + +- Affects every API route and utility importing `lib/db.ts`, plus `lib/auth.ts`. +- Affects the shared server database client, Prisma schema/model mappings where gaps are discovered, and database-related tests. +- Removes the application’s generic SQL query abstraction and likely removes direct `mysql2` usage after confirming Better Auth’s adapter requirements. +- Requires verification across type-checking, unit tests, production build, representative read/write API routes, authentication hooks, transactions, and database error handling. +- No database migration or user-facing API change is intended; any schema correction required to faithfully model existing columns must be separately reviewed before implementation. diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/raw-query-exceptions.md b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/raw-query-exceptions.md new file mode 100644 index 00000000..4ac857ac --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/raw-query-exceptions.md @@ -0,0 +1,11 @@ +# Typed Prisma raw-query exceptions + +The remaining raw SQL uses Prisma's `$queryRaw` or `$queryRawUnsafe` with explicit result types. These queries remain SQL because the legacy MySQL implementation relies on date functions, CTEs, grouped report aggregates, or multi-join projections that would otherwise change response semantics. + +- Monthly count/report routes: MySQL `YEAR`, `QUARTER`, `STR_TO_DATE`, and BigInt aggregate normalization. +- `getBatchesWithEnrollCount`, `getCoursesWithEnrollCount`, and `getYearlyEnrollmentTrend`: grouped dashboard aggregates. +- `getbatchdetails`: five legacy multi-join projections used to preserve the existing response contract. +- `getdocumentsfee`: the document-field projection must tolerate legacy empty strings that cannot be decoded as the current `YesNo` Prisma enum. +- `getbatchesdata`, `getbatchesreport`, and `getstudentsdata`: complex CTE/report projections with role filters and grouped financial/attendance metrics. + +All request-derived values are passed as bound parameters. The dynamic fragments in `getBatchesWithEnrollCount` are selected from fixed application branches, not request-provided identifiers. No generic database helper remains. diff --git a/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/tasks.md b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/tasks.md new file mode 100644 index 00000000..0dac235e --- /dev/null +++ b/openspec/changes/archive/2026-09-16-switch-db-queries-to-prisma/tasks.md @@ -0,0 +1,48 @@ +## 1. Baseline and Prisma foundation + +- [x] 1.1 Inventory every `executeQuery`, `lib/db`, direct `mysql2`, and SQL call in application code, group callers by domain, and verify the inventory is committed in the change notes or task evidence. +- [x] 1.2 Capture baseline `pnpm type-check`, `pnpm test`, `pnpm build`, and representative API/auth response fixtures, recording any pre-existing failures so migration regressions are distinguishable. +- [x] 1.3 Audit Prisma models, relations, enums, nullability, database column mappings, and generated-client availability against the migration SQL and all inventory queries; verify any required model correction is explicitly identified before route conversion. +- [x] 1.4 Add the shared server-only Prisma client singleton with development hot-reload reuse and production-safe lifecycle behavior; verify Prisma generation and a focused client initialization test/check succeed. +- [x] 1.5 Define the database-access conventions for typed `select`/`include`, request validation, response normalization, `$transaction`, and the documented parameterized raw-SQL exception; verify the conventions are applied in one pilot route. + +## 2. Configuration, courses, batches, and user domains + +- [x] 2.1 Convert dropdown and batch-status configuration utilities to Prisma model operations, preserving uniqueness checks, ordering, upserts, and validation errors; verify their unit tests and type-check pass. +- [x] 2.2 Convert course creation, update, deletion, listing, and course-count routes to typed Prisma queries while preserving response keys and status codes; verify targeted course API tests pass. +- [x] 2.3 Convert batch creation, listing, update, deletion, details, reports, enrollment counts, and yearly-trend routes, using typed relations/aggregates or documented raw-SQL exceptions; verify targeted batch/report tests and response fixtures pass. +- [x] 2.4 Convert user listing, creation, update, deletion, dropdown, and count routes to generated `vausers` types and enum values; verify targeted user API tests and type-check pass. + +## 3. Students and enrollment + +- [x] 3.1 Convert student lookup, detail, listing, unassigned-student, duplicate-check, document/fee, and student-count routes to typed `vastudents` selections and relations; verify nullable fields and serialized date/BigInt response fixtures match the baseline. +- [x] 3.2 Convert student application and bulk-registration writes to Prisma `create`/`createMany` or typed transactions, preserving legacy field normalization and duplicate behavior; verify success, validation failure, and duplicate scenarios. +- [x] 3.3 Convert add/remove-student-to-batch and batch-status mutations to typed relation operations and transactions, preserving attendance, grade, fee, and student status side effects; verify mutation tests cover success and partial-failure behavior. + +## 4. Attendance, grades, fees, and remarks + +- [x] 4.1 Convert attendance patch and batch-attendance routes to typed updates/upserts/transactions, preserving date handling, status codes, and existing no-duplicate behavior; verify representative present/absent/cancelled scenarios. +- [x] 4.2 Convert assignment creation/update/deletion and grade update routes to generated `va_assignments`/`va_grades` types; verify grade validation, lookup, mutation, and response behavior. +- [x] 4.3 Convert fee and document update routes to typed `va_fees` and `vastudents` mutations; verify all fee fields, nullable student IDs, and error responses against baseline fixtures. +- [x] 4.4 Convert VA remarks and telecaller remarks list/create/update/delete routes to typed relations and ownership filters; verify authorization/ownership failures and serialized response shapes. + +## 5. Reports, metrics, and audit + +- [x] 5.1 Convert monthly count routes for batches, courses, enrollments, instructors, leads, and TAs to typed aggregate queries; verify count values and zero-result behavior. +- [x] 5.2 Convert dashboard/report data routes and complex enrollment/payment metrics to Prisma aggregates, relations, and explicitly typed parameterized raw queries only where required; verify representative report totals and date filters match baseline fixtures. +- [x] 5.3 Convert audit create/get routes to typed `va_audit_logs` operations, preserving JSON details, filtering, pagination, and count behavior; verify audit payload and pagination tests. + +## 6. Authentication and dependency cleanup + +- [x] 6.1 Replace `findVaUser` in `lib/auth.ts` with a Prisma `vausers` lookup while preserving case-insensitive email matching and custom session/user fields; verify sign-in, session refresh, missing-user, and revoked-access scenarios. +- [x] 6.2 Determine whether the installed Better Auth version has a supported Prisma adapter for the existing auth models; if compatible, migrate and verify all auth tables/hooks, otherwise keep the adapter’s required MySQL integration isolated and document why it remains. +- [x] 6.3 Remove `lib/db.ts`, generic query result types, and all obsolete imports after repository searches show no application callers; verify `rg` finds no `@/lib/db`, `executeQuery`, or accidental query-row typing. +- [x] 6.4 Remove `mysql2` from runtime dependencies only if Better Auth and all remaining application code no longer require it; verify a clean dependency install and production build. + +## 7. Validation and rollout + +- [x] 7.1 Add or update unit/integration tests for Prisma query modules, response normalization, transaction failure handling, raw-query exceptions, and client lifecycle; verify `pnpm test` passes. +- [x] 7.2 Run `pnpm type-check` and confirm database-backed code uses generated Prisma types without broad `unknown`/generic row fallbacks or unvalidated request data in Prisma inputs. +- [x] 7.3 Run formatting, lint, Prisma validation/generation, and `pnpm build`; verify no route discovery, server/client boundary, or serialization errors are introduced. +- [x] 7.4 Run representative authenticated read/write/report smoke checks against the test database and compare response fixtures, mutation side effects, and error/status behavior with the baseline. +- [x] 7.5 Perform a final repository audit for unapproved SQL, direct connection creation, stale dependency/config references, and undocumented raw-query exceptions; record the final verification results in the change handoff. diff --git a/package.json b/package.json index bba930e3..5d2979f6 100644 --- a/package.json +++ b/package.json @@ -22,20 +22,22 @@ }, "dependencies": { "@better-auth/passkey": "^1.7.1", + "@better-auth/prisma-adapter": "1.7.1", "@emotion/react": "11.14.0", "@emotion/styled": "11.14.1", "@mui/icons-material": "6.5.0", "@mui/material": "6.5.0", "@mui/styled-engine-sc": "7.3.2", + "@prisma/adapter-mariadb": "7.10.0", "@prisma/client": "7.10.0", "@swc/helpers": "0.5.17", "ag-grid-community": "32.2.2", "ag-grid-react": "32.2.2", "better-auth": "^1.7.1", "date-fns": "4.1.0", + "mariadb": "^3.5.4", "moment": "2.30.1", "moment-timezone": "^0.5.48", - "mysql2": "3.9.8", "next": "^14.2.33", "next-pwa": "5.6.0", "react": "18.2.0", diff --git a/pages/api/addassignment.ts b/pages/api/addassignment.ts index 756ade96..83a96d94 100644 --- a/pages/api/addassignment.ts +++ b/pages/api/addassignment.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const body = req.body; @@ -8,9 +8,9 @@ export default async function handler(req, res) { return res.status(400).json({ success: false, message: "batch_id is required." }); } - const students = await executeQuery({ - query: "SELECT s.id FROM vastudents s JOIN vastudent_to_batch sb ON s.id = sb.student_id WHERE sb.batch_id = ?", - values: [body.batch_id], + const students = await prisma.vastudent_to_batch.findMany({ + where: { batch_id: body.batch_id }, + select: { student_id: true }, }); if (students.length === 0) { @@ -20,21 +20,22 @@ export default async function handler(req, res) { }); } - // Bulk insert for better performance - insert all students at once - const placeholders = students.map(() => "(?, ?, ?, ?, ?, ?, ?)").join(","); - const values = students.flatMap((student) => [ - student.id, - body.batch_id, - body.assignment_name, - body.assignment_type, - body.assignment_weight, - 0, - body.max_marks, - ]); - - await executeQuery({ - query: `INSERT INTO va_grades (student_id, batch_id, assignment_name, assignment_type, assignment_weight, grade, max_marks) VALUES ${placeholders}`, - values, + await prisma.va_grades.createMany({ + data: students.flatMap(({ student_id }) => + student_id == null + ? [] + : [ + { + student_id, + batch_id: body.batch_id, + assignment_name: body.assignment_name, + assignment_type: body.assignment_type, + assignment_weight: body.assignment_weight, + grade: 0, + max_marks: body.max_marks, + }, + ] + ), }); res.status(200).json({ success: true }); diff --git a/pages/api/addremark.ts b/pages/api/addremark.ts index 2258f89c..955ba132 100644 --- a/pages/api/addremark.ts +++ b/pages/api/addremark.ts @@ -1,15 +1,15 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const body = req.body; try { - const studentToBatch = await executeQuery({ - query: "SELECT id FROM vastudent_to_batch WHERE batch_id = ? AND student_id = ?", - values: [parseInt(body.batchId), parseInt(body.studentId)], + const studentToBatch = await prisma.vastudent_to_batch.findFirst({ + where: { batch_id: parseInt(body.batchId), student_id: parseInt(body.studentId) }, + select: { id: true }, }); - const id = studentToBatch[0]?.id; + const id = studentToBatch?.id; if (!id) { return res.status(404).json({ success: false, @@ -17,24 +17,18 @@ export default async function handler(req, res) { }); } if (body.isUpdated) { - // Delete all past remarks - await executeQuery({ - query: "DELETE FROM va_remarks WHERE vastudent_to_batch_id = ? and user_id = ?", - values: [id, body.commenter], + await prisma.va_remarks.deleteMany({ where: { vastudent_to_batch_id: id, user_id: body.commenter } }); + await prisma.va_remarks.createMany({ + data: body.remarksArray.map((remarks) => ({ + vastudent_to_batch_id: id, + remarks, + user_id: body.commenter, + })), }); - - // Re-insert past remarks - for (const pastRemark of body.remarksArray) { - await executeQuery({ - query: "INSERT INTO va_remarks (vastudent_to_batch_id, remarks, user_id) VALUES (?, ?, ?)", - values: [id, pastRemark, body.commenter], - }); - } } if (body.remarks !== "") { - await executeQuery({ - query: "INSERT INTO va_remarks (vastudent_to_batch_id, remarks, user_id) VALUES (?, ?, ?)", - values: [id, body.remarks, body.commenter], + await prisma.va_remarks.create({ + data: { vastudent_to_batch_id: id, remarks: body.remarks, user_id: body.commenter }, }); } diff --git a/pages/api/addstudenttobatch.ts b/pages/api/addstudenttobatch.ts index a1b52dfb..c6ee0b5c 100644 --- a/pages/api/addstudenttobatch.ts +++ b/pages/api/addstudenttobatch.ts @@ -1,7 +1,8 @@ // NOTE: If default data should be entered when student is added to // a batch, MySQL table column default and default value below must BOTH be set. -import { executeQuery } from "@/lib/db"; +import { vastudents_enrollment_status, YesNo } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; import { parseCourseDays, isClassDay } from "@/utils/course-days"; function formatDate(date) { @@ -42,21 +43,9 @@ async function createAttendanceRecords(batch_id, student_id, coursestart, course const dateArray = generateDateArray(coursestart, courseend, coursedays); if (!dateArray.length) return; - const placeholders = dateArray.map(() => "(?, ?, ?, ?)").join(","); - - // Per request, set the db value to 1 (Present) for default/initial data - // But this will show as "-" for future dates in the UI - // And can be updated as needed by the instructor - const values = dateArray.flatMap((date) => [batch_id, student_id, date, 1]); - try { - await executeQuery({ - query: ` - INSERT INTO va_attendance (batch_id, student_id, date, is_present) - VALUES ${placeholders} - ON DUPLICATE KEY UPDATE is_present = VALUES(is_present) - `, - values, + await prisma.va_attendance.createMany({ + data: dateArray.map((date) => ({ batch_id, student_id, date: new Date(date), is_present: 1 })), }); } catch (error) { console.log("Error inserting attendance records:", error); @@ -67,73 +56,58 @@ export default async function handler(req, res) { const { studentId, batchId } = req.body; try { - const existingStudentBatch = await executeQuery({ - query: ` - SELECT 1 - FROM vastudent_to_batch - WHERE student_id = ? - LIMIT 1 - `, - values: [studentId], + const existingStudentBatch = await prisma.vastudent_to_batch.findFirst({ + where: { student_id: studentId }, + select: { id: true }, }); - const studentAlreadyInBatch = existingStudentBatch.length > 0; - await executeQuery({ - query: "INSERT INTO vastudent_to_batch (student_id, batch_id) VALUES (?, ?)", - values: [studentId, batchId], - }); + const studentAlreadyInBatch = Boolean(existingStudentBatch); + await prisma.vastudent_to_batch.create({ data: { student_id: studentId, batch_id: batchId } }); // Get the student ID proof, disability certificate, and photo information - const getStudentIdInfo = await executeQuery({ - query: "SELECT id_proof, disability_cert, photo FROM vastudents WHERE id = ?", - values: [studentId], + const getStudentIdInfo = await prisma.vastudents.findUnique({ + where: { id: studentId }, + select: { id_proof: true, disability_cert: true, photo: true }, }); - const idProof = getStudentIdInfo[0]?.id_proof || "yes"; - const disabilityCert = getStudentIdInfo[0]?.disability_cert || "yes"; - const photo = getStudentIdInfo[0]?.photo || "yes"; + const idProof = getStudentIdInfo?.id_proof || YesNo.Yes; + const disabilityCert = getStudentIdInfo?.disability_cert || YesNo.Yes; + const photo = getStudentIdInfo?.photo || YesNo.Yes; // If student is already in the batch, we update their ID proof, disability certificate, and photo information // otherwise, we set them to "yes" as default values - await executeQuery({ - query: ` - UPDATE vastudents - SET id_proof = ?, disability_cert = ?, photo = ? - WHERE id = ? - `, - values: studentAlreadyInBatch ? [idProof, disabilityCert, photo, studentId] : ["yes", "yes", "yes", studentId], + await prisma.vastudents.update({ + where: { id: studentId }, + data: studentAlreadyInBatch + ? { id_proof: idProof, disability_cert: disabilityCert, photo } + : { id_proof: YesNo.Yes, disability_cert: YesNo.Yes, photo: YesNo.Yes }, }); - const result = await executeQuery({ - query: "SELECT DISTINCT assignment_name FROM va_grades WHERE batch_id = ? AND assignment_name IS NOT NULL", - values: [batchId], + const result = await prisma.va_grades.findMany({ + where: { batch_id: batchId }, + distinct: ["assignment_name"], + select: { assignment_name: true }, }); const assignments = result.map((assignment) => assignment.assignment_name); - const courseDatesResult = await executeQuery({ - query: "SELECT coursestart, courseend, coursedays FROM vabatches WHERE id = ?", - values: [batchId], + const courseDatesResult = await prisma.vabatches.findUnique({ + where: { id: batchId }, + select: { coursestart: true, courseend: true, coursedays: true }, }); - if (!courseDatesResult || courseDatesResult.length === 0) { + if (!courseDatesResult) { return res.status(404).json({ success: false, message: "Batch not found" }); } - const coursestart = courseDatesResult[0].coursestart; - const courseend = courseDatesResult[0].courseend; - const coursedaysValue = courseDatesResult[0].coursedays; + const coursestart = courseDatesResult.coursestart; + const courseend = courseDatesResult.courseend; + const coursedaysValue = courseDatesResult.coursedays; const coursedays = parseCourseDays(typeof coursedaysValue === "string" ? coursedaysValue : ""); - await executeQuery({ - query: ` - UPDATE vastudents AS s - JOIN vabatches AS b ON b.id = ? - SET s.enrollment_status = 'ENROLLED' - WHERE s.id = ? - AND (b.courseend IS NULL OR b.courseend >= CURDATE()) - `, - values: [batchId, studentId], + await prisma.vastudents.updateMany({ + where: { id: studentId, enrollment_status: { not: vastudents_enrollment_status.DROPOUT } }, + data: { enrollment_status: vastudents_enrollment_status.ENROLLED }, }); if (coursestart && courseend) { @@ -143,9 +117,16 @@ export default async function handler(req, res) { if (assignments && assignments.length > 0) { for (const assignment of assignments) { if (typeof assignment !== "undefined") { - await executeQuery({ - query: "INSERT INTO va_grades (student_id, batch_id, assignment_name) VALUES (?, ?, ?)", - values: [studentId, batchId, assignment], + await prisma.va_grades.create({ + data: { + student_id: studentId, + batch_id: batchId, + assignment_name: assignment, + assignment_type: "", + assignment_weight: 0, + grade: 0, + max_marks: 0, + }, }); } else { console.log("skipping"); @@ -154,9 +135,16 @@ export default async function handler(req, res) { } try { - await executeQuery({ - query: `INSERT INTO va_fees (batch_id, student_id, fee_paid) VALUES (?, ?, ?)`, - values: [batchId, studentId, "NA"], + await prisma.va_fees.create({ + data: { + batch_id: batchId, + student_id: studentId, + fee_paid: "NA", + amount_1: 0, + amount_2: 0, + amount_3: 0, + nature_of_fee: "", + }, }); } catch (error) { console.log("Error inserting fees record:", error); diff --git a/pages/api/audit/create.ts b/pages/api/audit/create.ts index 1bca2cd3..15d6690c 100644 --- a/pages/api/audit/create.ts +++ b/pages/api/audit/create.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method !== "POST") { @@ -15,28 +15,25 @@ export default async function handler(req, res) { }); } - const query = ` - INSERT INTO va_audit_logs - (action_type, performed_by, details, resource_type, resource_id) - VALUES (?, ?, ?, ?, ?) - `; - - // Convert details object to JSON string if it exists - const jsonDetails = details ? JSON.stringify(details) : null; - - const values = [action_type, performed_by, jsonDetails, resource_type || null, resource_id || null]; - - const results = await executeQuery({ query, values }); + const result = await prisma.va_audit_logs.create({ + data: { + action_type, + performed_by, + details: details ?? null, + resource_type: resource_type || null, + resource_id: resource_id == null ? null : String(resource_id), + }, + }); return res.status(201).json({ message: "Audit log entry created successfully", - id: results.insertId, + id: Number(result.id), }); } catch (error) { console.error("Error in audit log creation:", error); return res.status(500).json({ message: "Internal server error", - error: error.message, + error: error instanceof Error ? error.message : String(error), }); } } diff --git a/pages/api/audit/get.ts b/pages/api/audit/get.ts index 527f4815..8cd11467 100644 --- a/pages/api/audit/get.ts +++ b/pages/api/audit/get.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method !== "GET") { @@ -17,77 +17,48 @@ export default async function handler(req, res) { offset = 0, } = req.query; - let query = "SELECT * FROM va_audit_logs WHERE 1=1"; - const values: unknown[] = []; + const parsedLimit = Number(limit); + const parsedOffset = Number(offset); + const where = { + ...(typeof action_type === "string" ? { action_type } : {}), + ...(typeof performed_by === "string" ? { performed_by } : {}), + ...(typeof resource_type === "string" ? { resource_type } : {}), + ...(typeof resource_id === "string" ? { resource_id } : {}), + ...(typeof start_date === "string" || typeof end_date === "string" + ? { + performed_at: { + ...(typeof start_date === "string" ? { gte: new Date(start_date) } : {}), + ...(typeof end_date === "string" ? { lte: new Date(end_date) } : {}), + }, + } + : {}), + }; - // Add filters if provided - if (action_type) { - query += " AND action_type = ?"; - values.push(action_type); - } + const [total, mainResults] = await Promise.all([ + prisma.va_audit_logs.count({ where }), + prisma.va_audit_logs.findMany({ + where, + orderBy: { performed_at: "desc" }, + skip: Number.isFinite(parsedOffset) ? parsedOffset : 0, + take: Number.isFinite(parsedLimit) ? parsedLimit : 50, + }), + ]); - if (performed_by) { - query += " AND performed_by = ?"; - values.push(performed_by); - } - - if (resource_type) { - query += " AND resource_type = ?"; - values.push(resource_type); - } - - if (resource_id) { - query += " AND resource_id = ?"; - values.push(resource_id); - } - - if (start_date) { - query += " AND performed_at >= ?"; - values.push(new Date(start_date)); - } - - if (end_date) { - query += " AND performed_at <= ?"; - values.push(new Date(end_date)); - } - - // Add ordering and pagination - query += " ORDER BY performed_at DESC LIMIT ? OFFSET ?"; - values.push(parseInt(limit), parseInt(offset)); - - // Get total count for pagination - const countQuery = query.replace("SELECT *", "SELECT COUNT(*) as total").split("LIMIT")[0]; - - const countResults = await executeQuery({ - query: countQuery, - values: values.slice(0, -2), - }); - - // Execute main query - const mainResults = await executeQuery({ - query, - values, - }); - - // Parse JSON details field - const formattedResults = mainResults.map((row) => ({ - ...row, - details: row.details, - })); + const formattedResults = mainResults.map((row) => ({ ...row, id: Number(row.id) })); return res.status(200).json({ - total: countResults[0].total, + total, data: formattedResults, pagination: { - limit: parseInt(limit), - offset: parseInt(offset), + limit: Number.isFinite(parsedLimit) ? parsedLimit : 50, + offset: Number.isFinite(parsedOffset) ? parsedOffset : 0, }, }); } catch (error) { console.error("Error in audit log retrieval:", error); return res.status(500).json({ message: "Internal server error", - error: error.message, + error: error instanceof Error ? error.message : String(error), }); } } diff --git a/pages/api/batchcreate.ts b/pages/api/batchcreate.ts index 153db2f3..50e7c5d5 100644 --- a/pages/api/batchcreate.ts +++ b/pages/api/batchcreate.ts @@ -3,7 +3,13 @@ This function is called from batches.tsx (Batches link). It CREATES A NEW BATCH. */ -import { executeQuery } from "@/lib/db"; +import { vabatches_currency, vabatches_status, vabatches_trainingmode } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; + +const isCurrency = (value: unknown): value is vabatches_currency => + typeof value === "string" && Object.values(vabatches_currency).some((item) => item === value); +const isTrainingMode = (value: unknown): value is vabatches_trainingmode => + typeof value === "string" && Object.values(vabatches_trainingmode).some((item) => item === value); export default async function handler(req, res) { try { @@ -12,12 +18,12 @@ export default async function handler(req, res) { // Check if batch ID already exists if (body.batch) { - const existingBatch = await executeQuery({ - query: "SELECT id FROM vabatches WHERE batch = ?", - values: [body.batch.trim()], + const existingBatch = await prisma.vabatches.findFirst({ + where: { batch: body.batch.trim() }, + select: { id: true }, }); - if (existingBatch && existingBatch.length > 0) { + if (existingBatch) { return res.status(400).json({ success: false, message: "Batch ID already exists. Please use a different Batch ID.", @@ -32,33 +38,29 @@ export default async function handler(req, res) { } else { coursedays = body.coursedays; } - const result = await executeQuery({ - /* ---------- DATABASE MODIFICATION SECTION ------------- */ - // If timestamp is a field, use: user.createdAt.Date (not toString) - // NOTE: coursedays does is not a property of 'body' in query below; - // Do not use body.coursedays just use coursedays - query: - "INSERT INTO vabatches (id, coursename, batch, coursestart, courseend, coursedays, coursetimes, instructor, PM, TA, dataentry, cost, currency, strength, trainingmode, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - values: [ - null, - body.coursename || "", - body.batch || "", - body.coursestart || "", - body.courseend || "", - coursedays || "", - (body.coursetimestart ? body.coursetimestart + " - " + (body.coursetimeend || "") : "") || "", - body.instructor || "", - body.PM || "", - body.TA || "", - body.dataentry || "", - body.cost || null, - body.currency || null, - body.strength || 0, - body.trainingmode || "", - "UNSTARTED", - ], + if (!isTrainingMode(body.trainingmode)) { + return res.status(400).json({ success: false, message: "Invalid training mode" }); + } + const result = await prisma.vabatches.create({ + data: { + coursename: body.coursename || "", + batch: body.batch || "", + coursestart: body.coursestart || "", + courseend: body.courseend || "", + coursedays: coursedays || "", + coursetimes: (body.coursetimestart ? body.coursetimestart + " - " + (body.coursetimeend || "") : "") || "", + instructor: body.instructor || "", + PM: body.PM || "", + TA: body.TA || "", + dataentry: body.dataentry || "", + cost: body.cost || null, + currency: isCurrency(body.currency) ? body.currency : null, + strength: body.strength || 0, + trainingmode: body.trainingmode, + status: vabatches_status.UNSTARTED, + }, }); - res.status(200).json({ success: true, batchId: result.insertId }); + res.status(200).json({ success: true, batchId: result.id }); } catch (error) { console.log(error); res.status(500).json({ success: false, message: "An error occurred" }); diff --git a/pages/api/batches.ts b/pages/api/batches.ts index ae201c62..614d887e 100644 --- a/pages/api/batches.ts +++ b/pages/api/batches.ts @@ -3,17 +3,14 @@ This function is called from the Courses page (/courses.tsx). It executes the query that SHOWS CURRENT COURSES (?CURRENT BATCHES). */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function assetHandler(req, res) { const { method } = req; switch (method) { case "GET": try { - const result = await executeQuery({ - query: "SELECT * FROM vabatches", - values: [], - }); + const result = await prisma.vabatches.findMany(); res.json(result); return result; } catch (e) { diff --git a/pages/api/bulkregcheckstudentduplicates.ts b/pages/api/bulkregcheckstudentduplicates.ts index 4f3d92c1..518c7042 100644 --- a/pages/api/bulkregcheckstudentduplicates.ts +++ b/pages/api/bulkregcheckstudentduplicates.ts @@ -3,7 +3,7 @@ This function is called from bulkstudentregistration.tsx (Bulk Registration link student bulk registration checking for duplicates in the database. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { @@ -12,9 +12,6 @@ export default async function handler(req, res) { // console.log("Received rows for duplicate check:", rows); - const dupConditions: string[] = []; - const dupValues: unknown[] = []; - if (!Array.isArray(rows) || rows.length === 0) { return res.status(200).json({ success: true, @@ -28,36 +25,22 @@ export default async function handler(req, res) { // you can check this by running // SHOW INDEX FROM vastats.vastudents; // and looking for the unique key constraint - rows.forEach((row) => { - dupConditions.push("(gender = ? AND age = ? AND phone_number = ?)"); - dupValues.push(row.gender || null, row.age || null, row.phone_number || null); - }); - - const whereClause = dupConditions.join(" OR "); - // console.log("WHERE clause for duplicate check:", whereClause); - - const query = ` - SELECT - id, gender, cast(age as char) as age, phone_number - FROM - vastudents - WHERE - ${whereClause} - `; - - console.log("Executing duplicate check query:", query); - console.log("With values:", dupValues); - - const duplicates = await executeQuery({ - query: query, - values: dupValues, + const duplicates = await prisma.vastudents.findMany({ + where: { + OR: rows.map((row) => ({ + gender: row.gender || null, + age: row.age ? new Date(row.age) : null, + phone_number: row.phone_number ? BigInt(row.phone_number) : null, + })), + }, + select: { id: true, gender: true, age: true, phone_number: true }, }); - console.log("Duplicate check query result:", duplicates); - // compare query findings with input data // updating row._errors._database_duplicates with any error messages if duplicates are found - const duplicatesSet = new Set(duplicates.map((dup) => `${dup.gender}|${dup.age}|${dup.phone_number}`)); + const duplicatesSet = new Set( + duplicates.map((dup) => `${dup.gender}|${dup.age?.toISOString().slice(0, 10)}|${dup.phone_number}`) + ); console.log("Set of existing students for duplicate check:", duplicatesSet); rows.forEach((row) => { diff --git a/pages/api/bulkstudentapplication.ts b/pages/api/bulkstudentapplication.ts index e7f8a205..ebfe0809 100644 --- a/pages/api/bulkstudentapplication.ts +++ b/pages/api/bulkstudentapplication.ts @@ -3,7 +3,7 @@ This function is called from students.tsx (Students link). bulk student registration form. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { @@ -18,10 +18,7 @@ export default async function handler(req, res) { // const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' '); const currentDate = new Date(); - const allValues: unknown[] = []; - const valuePlaceholders: string[] = []; - - students.forEach((body) => { + const data = students.map((body) => { // Format the age (DOB) to YYYY-MM-DD without timezone const formattedAge = body.age ? new Date(body.age).toISOString().split("T")[0] : null; @@ -35,73 +32,52 @@ export default async function handler(req, res) { .join(" ") : null; - const values = [ - body.Quarter || null, - body.SNo || null, - body.Batch_ID || null, - body.Program_Name || null, - body.Trainee_ID || null, - rowname || null, - body.edu_qualifications || null, - body.phone_number || null, - body.alt_ph_num || null, - body.CityDistrict_State || null, - body.email || null, - body.gender || null, - formattedAge, - body.visual_acuity || null, - body.percent_loss || null, - body.employment_status || null, - body.Designation || null, - body.Languages_Known || null, - body.Program_ManagerCoordinator || null, - body.country || null, - body.state || null, - body.city || null, - body.disability || null, - body.edu_details || "", - body.objectives || null, - body.first_choice || null, - body.second_choice || null, - body.third_choice || null, - body.impairment_history || null, - body.source || null, - currentDate, - body.id_proof || "", - body.disability_cert || "", - body.photo || "", - body.bank_details || "", - body.completion_status || null, - body.reason_for_status || null, - body.certification_eligibility || null, - body.risk_factor || null, - body.remarks || null, - body.commenter || null, - ]; - - allValues.push(...values); - valuePlaceholders.push( - "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - ); + return { + Quarter: body.Quarter || null, + SNo: body.SNo || null, + Batch_ID: body.Batch_ID || null, + Program_Name: body.Program_Name || null, + Trainee_ID: body.Trainee_ID || null, + name: rowname || null, + edu_qualifications: body.edu_qualifications || null, + phone_number: body.phone_number ? BigInt(body.phone_number) : null, + alt_ph_num: body.alt_ph_num ? BigInt(body.alt_ph_num) : null, + CityDistrict_State: body.CityDistrict_State || null, + email: body.email || null, + gender: body.gender || null, + age: formattedAge ? new Date(formattedAge) : null, + visual_acuity: body.visual_acuity || null, + percent_loss: body.percent_loss || null, + employment_status: body.employment_status || null, + Designation: body.Designation || null, + Languages_Known: body.Languages_Known || null, + Program_ManagerCoordinator: body.Program_ManagerCoordinator || null, + country: body.country || null, + state: body.state || null, + city: body.city || null, + disability: body.disability || "", + edu_details: body.edu_details || "", + objectives: body.objectives || null, + first_choice: body.first_choice || null, + second_choice: body.second_choice || null, + third_choice: body.third_choice || null, + impairment_history: body.impairment_history || null, + source: body.source || null, + registration_date: currentDate, + id_proof: body.id_proof || "", + disability_cert: body.disability_cert || "", + photo: body.photo || "", + bank_details: body.bank_details || "", + completion_status: body.completion_status || null, + reason_for_status: body.reason_for_status || null, + certification_eligibility: body.certification_eligibility || null, + risk_factor: body.risk_factor || null, + remarks: body.remarks || null, + commenter: body.commenter || null, + }; }); - const result = await executeQuery({ - /*----- COLS: 42 NOT including: id -----*/ - query: ` - INSERT INTO vastudents ( - Quarter, SNo, Batch_ID, Program_Name, Trainee_ID, name, - edu_qualifications, phone_number, alt_ph_num, CityDistrict_State, - email, gender, age, visual_acuity, percent_loss, employment_status, - Designation, Languages_Known, Program_ManagerCoordinator, country, - state, city, disability, edu_details, objectives, first_choice, - second_choice, third_choice, impairment_history, source, - registration_date, id_proof, disability_cert, photo, bank_details, - completion_status, reason_for_status, certification_eligibility, - risk_factor, remarks, commenter - ) VALUES ${valuePlaceholders.join(", ")} - `, - values: allValues, - }); + const result = await prisma.vastudents.createMany({ data }); // Return a JSON success response instead of redirecting res.status(200).json({ diff --git a/pages/api/countbatch.ts b/pages/api/countbatch.ts index 523a190c..90ef2fd0 100644 --- a/pages/api/countbatch.ts +++ b/pages/api/countbatch.ts @@ -2,17 +2,14 @@ This function is called from index.tsx to count number of batches for overall stats */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(_req, res) { try { - const data = await executeQuery({ - query: "SELECT COUNT(*) FROM vabatches", - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + const count = await prisma.vabatches.count(); + res.status(200).json({ count }); } catch (error) { - console.log(error); + console.error(error); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countbatchesmonth.ts b/pages/api/countbatchesmonth.ts index a6e0ebbe..a44dd6e7 100644 --- a/pages/api/countbatchesmonth.ts +++ b/pages/api/countbatchesmonth.ts @@ -1,20 +1,17 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(*) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(*) AS count FROM vabatches b WHERE YEAR(b.coursestart) = YEAR(CURDATE()) AND QUARTER(b.coursestart) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countcourse.ts b/pages/api/countcourse.ts index c5c94ca3..63f71d96 100644 --- a/pages/api/countcourse.ts +++ b/pages/api/countcourse.ts @@ -1,19 +1,15 @@ -/* -This function is called from index.tsx to count number of students for overall stats -*/ +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "@/lib/prisma"; -import { executeQuery } from "@/lib/db"; - -export default async function handler(req, res) { +export default async function handler( + _req: NextApiRequest, + res: NextApiResponse<{ count: number } | { error: string }> +) { try { - const data = await executeQuery({ - query: "SELECT COUNT(*) FROM vacourses", - values: [], - }); - console.log(data[0]["COUNT(*)"]); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + const count = await prisma.vacourses.count(); + res.status(200).json({ count }); } catch (error) { - console.log(error); + console.error(error); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countcoursesmonth.ts b/pages/api/countcoursesmonth.ts index 2cc8a9b1..2e7b1650 100644 --- a/pages/api/countcoursesmonth.ts +++ b/pages/api/countcoursesmonth.ts @@ -1,21 +1,18 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(*) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(*) AS count FROM vabatches b WHERE TRIM(COALESCE(b.coursename, '')) <> '' AND YEAR(b.coursestart) = YEAR(CURDATE()) AND QUARTER(b.coursestart) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countenrollsmonth.ts b/pages/api/countenrollsmonth.ts index 8b8ca7e6..d722983a 100644 --- a/pages/api/countenrollsmonth.ts +++ b/pages/api/countenrollsmonth.ts @@ -1,22 +1,19 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(stb.student_id) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(stb.student_id) AS count FROM vabatches b JOIN vastudent_to_batch stb ON stb.batch_id = b.id WHERE YEAR(b.coursestart) = YEAR(CURDATE()) AND QUARTER(b.coursestart) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(stb.student_id)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countinstructorsmonth.ts b/pages/api/countinstructorsmonth.ts index 13a99ca4..5e14ea63 100644 --- a/pages/api/countinstructorsmonth.ts +++ b/pages/api/countinstructorsmonth.ts @@ -1,21 +1,18 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(*) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(*) AS count FROM vabatches b WHERE TRIM(COALESCE(b.instructor, '')) <> '' AND YEAR(b.coursestart) = YEAR(CURDATE()) AND QUARTER(b.coursestart) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countleadsmonth.ts b/pages/api/countleadsmonth.ts index 9258ed2e..750112fd 100644 --- a/pages/api/countleadsmonth.ts +++ b/pages/api/countleadsmonth.ts @@ -1,20 +1,17 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(*) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(*) AS count FROM vastudents WHERE YEAR(registration_date) = YEAR(CURDATE()) AND QUARTER(registration_date) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/countstudent.ts b/pages/api/countstudent.ts index 514a7bb3..58a158b0 100644 --- a/pages/api/countstudent.ts +++ b/pages/api/countstudent.ts @@ -2,18 +2,14 @@ This function is called from index.tsx to count number of students for overall stats */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(_req, res) { try { - const data = await executeQuery({ - query: "SELECT COUNT(*) FROM vastudents", - values: [], - }); - console.log(data[0]["COUNT(*)"]); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + const count = await prisma.vastudents.count(); + res.status(200).json({ count }); } catch (error) { - console.log(error); + console.error(error); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/counttasmonth.ts b/pages/api/counttasmonth.ts index 08898339..6b6ed17f 100644 --- a/pages/api/counttasmonth.ts +++ b/pages/api/counttasmonth.ts @@ -1,21 +1,18 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT COUNT(*) + const data = await prisma.$queryRaw<{ count: bigint }[]>(Prisma.sql` + SELECT COUNT(*) AS count FROM vabatches b WHERE TRIM(COALESCE(b.TA, '')) <> '' AND YEAR(b.coursestart) = YEAR(CURDATE()) AND QUARTER(b.coursestart) = QUARTER(CURDATE()) - `, - values: [], - }); - res.status(200).json({ count: data[0]["COUNT(*)"] }); - res.end(); + `); + res.status(200).json({ count: Number(data[0]?.count ?? BigInt(0)) }); } catch (error) { - console.log(error); - return res.status(500).json({ error: error.message }); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/coursecreate.ts b/pages/api/coursecreate.ts index 1f27634b..63e21d98 100644 --- a/pages/api/coursecreate.ts +++ b/pages/api/coursecreate.ts @@ -3,9 +3,22 @@ This function is called from courses.tsx (Courses link). It CREATES A COURSE. */ -import { executeQuery } from "@/lib/db"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { vacourses_duration_type } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +type CourseBody = { + course?: unknown; + description?: unknown; + duration?: unknown; + duration_type?: unknown; +}; + +function isDurationType(value: unknown): value is vacourses_duration_type { + return typeof value === "string" && Object.values(vacourses_duration_type).some((item) => item === value); +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== "POST") { return res.status(405).json({ message: "Method Not Allowed" }); } @@ -14,14 +27,25 @@ export default async function handler(req, res) { try { // Get data submitted in request body - const body = typeof req.body === "string" ? JSON.parse(req.body) : req.body; + const body: CourseBody = typeof req.body === "string" ? JSON.parse(req.body) : req.body; + + if ( + typeof body.course !== "string" || + typeof body.duration !== "string" || + typeof body.duration_type !== "string" || + !isDurationType(body.duration_type) + ) { + return res.status(400).json({ message: "Invalid course data" }); + } // View response object in terminal - await executeQuery({ - /* ---------- DATABASE MODIFICATION SECTION ------------- */ - // If timestamp is a field, use: user.createdAt.Date (not toString) - query: "INSERT INTO vacourses (id, course, description, duration, duration_type) VALUES (?, ?, ?, ?, ?)", - values: ["", body.course, body.description, body.duration, body.duration_type], + await prisma.vacourses.create({ + data: { + course: body.course, + description: typeof body.description === "string" ? body.description : null, + duration: body.duration, + duration_type: body.duration_type, + }, }); } catch (error) { console.log(error); diff --git a/pages/api/deleteassignment.ts b/pages/api/deleteassignment.ts index 3ec94510..73e0ae89 100644 --- a/pages/api/deleteassignment.ts +++ b/pages/api/deleteassignment.ts @@ -1,13 +1,10 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const { batchId, assignmentName } = req.body; try { - await executeQuery({ - query: "DELETE FROM va_grades WHERE batch_id = ? AND assignment_name = ?", - values: [batchId, assignmentName], - }); + await prisma.va_grades.deleteMany({ where: { batch_id: batchId, assignment_name: assignmentName } }); res.status(200).json({ success: true }); } catch (error) { diff --git a/pages/api/deletebatch.ts b/pages/api/deletebatch.ts index 5d50965f..0c47625f 100644 --- a/pages/api/deletebatch.ts +++ b/pages/api/deletebatch.ts @@ -3,23 +3,20 @@ This function is called from courses.tsx (Courses link). It CREATES A COURSE. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { // Get data submitted in request body - const body = req.body; + const body: { id?: unknown } = req.body; - await executeQuery({ - /* ---------- DATABASE MODIFICATION SECTION ------------- */ - // If timestamp is a field, use: user.createdAt.Date (not toString) - query: "DELETE FROM vabatches WHERE id = ?", - values: [body.id], - }); + if (typeof body.id !== "number") return res.status(400).json({ message: "Invalid batch id" }); + await prisma.vabatches.delete({ where: { id: body.id } }); res.status(200).json({ message: "Batch deleted successfully" }); res.end(); } catch (error) { - console.log(error); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } // Redirect program flow back to Courses page // Thanks, Ruben Leija, for the tip that helped me here: diff --git a/pages/api/deletecourse.ts b/pages/api/deletecourse.ts index 5a9f32ad..9a7b9ad3 100644 --- a/pages/api/deletecourse.ts +++ b/pages/api/deletecourse.ts @@ -3,25 +3,24 @@ This function is called from courses.tsx (Courses link). It CREATES A COURSE. */ -import { executeQuery } from "@/lib/db"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { // Get data submitted in request body - const body = req.body; + const body: { id?: unknown } = req.body; // View response object in terminal console.log("body: ", body); - await executeQuery({ - /* ---------- DATABASE MODIFICATION SECTION ------------- */ - // If timestamp is a field, use: user.createdAt.Date (not toString) - query: "DELETE FROM vacourses WHERE id = ?", - values: [body.id], - }); + if (typeof body.id !== "number") return res.status(400).json({ message: "Invalid course id" }); + + await prisma.vacourses.delete({ where: { id: body.id } }); res.status(200).json({ message: "Course deleted successfully" }); res.end(); } catch (error) { - console.log(error); + console.error(error); + return res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } // Redirect program flow back to Courses page // Thanks, Ruben Leija, for the tip that helped me here: diff --git a/pages/api/deletestudent.ts b/pages/api/deletestudent.ts index f2309495..aa1904dd 100644 --- a/pages/api/deletestudent.ts +++ b/pages/api/deletestudent.ts @@ -1,20 +1,14 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function deleteStudent(req, res) { try { const { id } = req.body; try { - const query = ` - DELETE FROM vastudents - WHERE id = ?; - `; - - const values = [id]; - await executeQuery({ query, values }); + await prisma.vastudents.delete({ where: { id } }); res.status(200).json({ message: "Student deleted successfully" }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } catch (error) { res.status(400).json({ message: "Invalid request method" }); diff --git a/pages/api/deletestudentfrombatch.ts b/pages/api/deletestudentfrombatch.ts index aa110b63..d13dd5bb 100644 --- a/pages/api/deletestudentfrombatch.ts +++ b/pages/api/deletestudentfrombatch.ts @@ -1,74 +1,35 @@ -import { executeQuery } from "@/lib/db"; +import { vabatches_status, vastudents_enrollment_status } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const { studentId, batchId } = req.body; try { - // Delete attendance records - try { - await executeQuery({ - query: "DELETE FROM va_attendance WHERE batch_id = ? AND student_id = ?", - values: [batchId, studentId], - }); - } catch (error) { - console.log("Error deleting attendance record:", error); - } - - // Delete grades records - try { - await executeQuery({ - query: "DELETE FROM va_grades WHERE batch_id = ? AND student_id = ?", - values: [batchId, studentId], - }); - } catch (error) { - console.log("Error deleting grades record:", error); - } - - // Delete fee records - try { - await executeQuery({ - query: "DELETE FROM va_fees WHERE batch_id = ? AND student_id = ?", - values: [batchId, studentId], - }); - } catch (error) { - console.log("Error deleting documents and fees record:", error); - } - - // Delete Student-batch link - try { - await executeQuery({ - query: "DELETE FROM vastudent_to_batch WHERE batch_id = ? AND student_id = ?", - values: [batchId, studentId], - }); - } catch (error) { - console.log("Error deleting student to batch record:", error); - } + await prisma.$transaction([ + prisma.va_attendance.deleteMany({ where: { batch_id: batchId, student_id: studentId } }), + prisma.va_grades.deleteMany({ where: { batch_id: batchId, student_id: studentId } }), + prisma.va_fees.deleteMany({ where: { batch_id: batchId, student_id: studentId } }), + prisma.vastudent_to_batch.deleteMany({ where: { batch_id: batchId, student_id: studentId } }), + ]); // Check if student is still enrolled in any current or future batch // If not, set enrollment_status to AVAILABLE try { - const activeBatches = await executeQuery({ - query: ` - SELECT 1 - FROM vabatches AS b - JOIN vastudent_to_batch AS sb ON b.id = sb.batch_id - WHERE sb.student_id = ? - AND (b.courseend IS NULL OR b.courseend >= CURDATE()) - AND (b.status IS NULL OR UPPER(b.status) <> 'COMPLETE') - LIMIT 1 - `, - values: [studentId], + const activeBatch = await prisma.vastudent_to_batch.findFirst({ + where: { + student_id: studentId, + vabatches: { + courseend: { gte: new Date().toISOString().slice(0, 10) }, + status: { not: vabatches_status.COMPLETE }, + }, + }, }); // If student is not in any current or future batch, set enrollment_status to AVAILABLE - if (!activeBatches || activeBatches.length === 0) { - await executeQuery({ - query: ` - UPDATE vastudents - SET enrollment_status = 'AVAILABLE' - WHERE id = ? AND enrollment_status = 'ENROLLED' - `, - values: [studentId], + if (!activeBatch) { + await prisma.vastudents.updateMany({ + where: { id: studentId, enrollment_status: vastudents_enrollment_status.ENROLLED }, + data: { enrollment_status: vastudents_enrollment_status.AVAILABLE }, }); } } catch (error) { diff --git a/pages/api/deleteuser.ts b/pages/api/deleteuser.ts index f0e422ac..27068b92 100644 --- a/pages/api/deleteuser.ts +++ b/pages/api/deleteuser.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; import { staffAuditLogger } from "../../utils/auditLogger"; @@ -16,42 +16,33 @@ export default async function deleteUser(req, res) { } try { - const performerData = await executeQuery({ - query: "SELECT id FROM vausers WHERE email = ?", - values: [session?.user?.email], - }); - const performerId = performerData?.[0]?.id || "Unknown User"; + const performer = session?.user?.email + ? await prisma.vausers.findUnique({ where: { email: session.user.email }, select: { id: true } }) + : null; + const performerId = performer?.id || "Unknown User"; - const userData = await executeQuery({ - query: "SELECT * FROM vausers WHERE id = ?", - values: [id], - }); + const userData = await prisma.vausers.findUnique({ where: { id }, include: { va_remarks: true } }); - if (!userData || userData.length === 0) { + if (!userData) { return res.status(404).json({ message: "User not found" }); } - const deleteRemarksResult = await executeQuery({ - query: "DELETE FROM va_remarks WHERE user_id = ?", - values: [id], - }); - - const deleteUserResult = await executeQuery({ - query: "DELETE FROM vausers WHERE id = ?", - values: [id], - }); + const [deleteRemarksResult, deleteUserResult] = await prisma.$transaction([ + prisma.va_remarks.deleteMany({ where: { user_id: id } }), + prisma.vausers.delete({ where: { id } }), + ]); + const { va_remarks: _remarks, ...deletedUser } = userData; try { - // Log the staff creation - await staffAuditLogger.logStaffDeletion(performerId, id, userData[0]); + await staffAuditLogger.logStaffDeletion(performerId, id, deletedUser); } catch (auditError) { - console.error("Audit logger threw:", auditError.message); - console.error("Audit stack:", auditError.stack); + console.error("Audit logger threw:", auditError instanceof Error ? auditError.message : String(auditError)); + console.error("Audit stack:", auditError instanceof Error ? auditError.stack : undefined); } return res.status(200).json({ message: "User and related records deleted successfully", - remarksDeleted: deleteRemarksResult.affectedRows, - userDeleted: deleteUserResult.affectedRows, + remarksDeleted: deleteRemarksResult.count, + userDeleted: deleteUserResult ? 1 : 0, }); } catch (error) { return res.status(500).json({ diff --git a/pages/api/dropdownDataPm.ts b/pages/api/dropdownDataPm.ts index 5ea0a2f7..67d24933 100644 --- a/pages/api/dropdownDataPm.ts +++ b/pages/api/dropdownDataPm.ts @@ -15,13 +15,15 @@ // }); // }; -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; async function pmManager(req, res) { try { - const results = await executeQuery({ - query: - "SELECT * FROM vausers WHERE designation REGEXP '^(Senior Program Manager|Program Manager|Program Coordinator)$' ORDER BY name ASC", + const results = await prisma.vausers.findMany({ + where: { + designation: { in: ["Senior Program Manager", "Program Manager", "Program Coordinator"] }, + }, + orderBy: { name: "asc" }, }); res.status(200).json(results); } catch (error) { diff --git a/pages/api/dropdownDataStaff.ts b/pages/api/dropdownDataStaff.ts index 762c6669..facb8f94 100644 --- a/pages/api/dropdownDataStaff.ts +++ b/pages/api/dropdownDataStaff.ts @@ -16,13 +16,11 @@ // }); // }; -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; async function staffList(req, res) { try { - const results = await executeQuery({ - query: "SELECT DISTINCT * FROM vausers ORDER BY name ASC", - }); + const results = await prisma.vausers.findMany({ orderBy: { name: "asc" } }); res.status(200).json(results); } catch (error) { console.error("Error fetching staff list:", error); diff --git a/pages/api/getBatchesWithEnrollCount.ts b/pages/api/getBatchesWithEnrollCount.ts index bd65721e..2fd996ac 100644 --- a/pages/api/getBatchesWithEnrollCount.ts +++ b/pages/api/getBatchesWithEnrollCount.ts @@ -13,7 +13,8 @@ includeEmpty=true|false */ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { @@ -26,50 +27,43 @@ export default async function handler(req, res) { }); } - // Select fields dynamically depending on use case - const selectFields = + const data = includeDetails === "true" - ? ` - b.id, - b.batch, - b.coursename, - b.trainingmode, - b.coursestart, - b.courseend, - b.coursedays, - b.status, - COUNT(stb.student_id) AS enrolled_students - ` - : ` - b.id, - b.batch, - COUNT(stb.student_id) AS enrolled_students - `; - - // Dashboard excludes empty batches by default - // Reports can include them with includeEmpty=true - const havingClause = includeEmpty === "true" ? "" : "HAVING COUNT(stb.student_id) > 0"; - - const data = await executeQuery({ - query: ` - SELECT - ${selectFields} + ? await prisma.$queryRaw< + Array<{ + id: number; + batch: string; + coursename: string; + trainingmode: string; + coursestart: string; + courseend: string; + coursedays: string; + status: string; + enrolled_students: bigint; + }> + >(Prisma.sql` + SELECT b.id, b.batch, b.coursename, b.trainingmode, b.coursestart, b.courseend, + b.coursedays, b.status, COUNT(stb.student_id) AS enrolled_students FROM vabatches b - LEFT JOIN vastudent_to_batch stb - ON b.id = stb.batch_id - WHERE - STR_TO_DATE(b.coursestart, '%Y-%m-%d') - BETWEEN ? AND ? + LEFT JOIN vastudent_to_batch stb ON b.id = stb.batch_id + WHERE STR_TO_DATE(b.coursestart, '%Y-%m-%d') BETWEEN ${startDate} AND ${endDate} GROUP BY b.id - ${havingClause} + ${includeEmpty === "true" ? Prisma.empty : Prisma.sql`HAVING COUNT(stb.student_id) > 0`} ORDER BY enrolled_students DESC; - `, - values: [startDate, endDate], - }); - - console.log("getBatchesWithEnrollCount - query result:", data); + `) + : await prisma.$queryRaw>(Prisma.sql` + SELECT b.id, b.batch, COUNT(stb.student_id) AS enrolled_students + FROM vabatches b + LEFT JOIN vastudent_to_batch stb ON b.id = stb.batch_id + WHERE STR_TO_DATE(b.coursestart, '%Y-%m-%d') BETWEEN ${startDate} AND ${endDate} + GROUP BY b.id + ${includeEmpty === "true" ? Prisma.empty : Prisma.sql`HAVING COUNT(stb.student_id) > 0`} + ORDER BY enrolled_students DESC; + `); - return res.status(200).json({ batches: data }); + return res + .status(200) + .json({ batches: data.map((batch) => ({ ...batch, enrolled_students: Number(batch.enrolled_students) })) }); } catch (error) { console.error("/api/getBatchesWithEnrollCount error:", error); return res.status(500).json({ error: "Internal Server Error" }); diff --git a/pages/api/getCoursesWithEnrollCount.ts b/pages/api/getCoursesWithEnrollCount.ts index d26b513d..ecf8eb93 100644 --- a/pages/api/getCoursesWithEnrollCount.ts +++ b/pages/api/getCoursesWithEnrollCount.ts @@ -3,30 +3,24 @@ Groups results by coursename and counts the number of enrolled students for each course. */ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` - SELECT - b.coursename, - COUNT(stb.student_id) AS enrolled_students + const data = await prisma.$queryRaw>(Prisma.sql` + SELECT b.coursename, COUNT(stb.student_id) AS enrolled_students FROM vabatches b - JOIN vastudent_to_batch stb - ON b.id = stb.batch_id - WHERE - (QUARTER(b.coursestart) = QUARTER(CURDATE()) AND YEAR(b.coursestart) = YEAR(CURDATE())) + JOIN vastudent_to_batch stb ON b.id = stb.batch_id + WHERE QUARTER(b.coursestart) = QUARTER(CURDATE()) AND YEAR(b.coursestart) = YEAR(CURDATE()) GROUP BY b.coursename HAVING COUNT(stb.student_id) > 0 ORDER BY enrolled_students DESC; - `, - values: [], - }); + `); - console.log("getCoursesWithEnrollCount - query result:", data); - - return res.status(200).json({ courses: data }); + return res + .status(200) + .json({ courses: data.map((course) => ({ ...course, enrolled_students: Number(course.enrolled_students) })) }); } catch (error) { console.error("/api/getCoursesWithEnrollCount error:", error); return res.status(500).json({ error: "Internal Server Error" }); diff --git a/pages/api/getYearlyEnrollmentTrend.ts b/pages/api/getYearlyEnrollmentTrend.ts index 2a88e721..01c6100b 100644 --- a/pages/api/getYearlyEnrollmentTrend.ts +++ b/pages/api/getYearlyEnrollmentTrend.ts @@ -3,12 +3,12 @@ Groups results by month and counts the number of enrolled students per month. */ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { - const data = await executeQuery({ - query: ` + const data = await prisma.$queryRaw>(Prisma.sql` SELECT MONTH(b.coursestart) AS month_number, DATE_FORMAT(b.coursestart, '%b') AS month, @@ -23,13 +23,9 @@ export default async function handler(req, res) { DATE_FORMAT(b.coursestart, '%b') ORDER BY MONTH(b.coursestart); - `, - values: [], - }); + `); - console.log("getYearlyEnrollmentTrend - query result:", data); - - return res.status(200).json({ trend: data }); + return res.status(200).json({ trend: data.map((row) => ({ ...row, enrollments: Number(row.enrollments) })) }); } catch (error) { console.error("/api/getYearlyEnrollmentTrend error:", error); return res.status(500).json({ error: "Internal Server Error" }); diff --git a/pages/api/getbatchdetails.ts b/pages/api/getbatchdetails.ts index 3e561659..d97a0837 100644 --- a/pages/api/getbatchdetails.ts +++ b/pages/api/getbatchdetails.ts @@ -1,6 +1,14 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { parseCourseDays, isClassDay } from "@/utils/course-days"; +type BatchDetailValue = string | number | bigint | boolean | Date | null; +type BatchDetailRow = Record; + +const serializeBatchDetailRow = (row: BatchDetailRow): Record => + Object.fromEntries( + Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? value.toString() : value]) + ); + // Format date as YYYY-MM-DD without timezone conversion function formatDate(date) { if (!date) return ""; @@ -56,6 +64,11 @@ function generateDateArray(startDate, endDate, courseDays) { export default async function handler(req, res) { const { batch_id } = req.body; + const batchId = Number(batch_id); + + if (!Number.isInteger(batchId) || batchId <= 0) { + return res.status(400).json({ error: "Invalid batch_id" }); + } try { const studentsQuery = ` @@ -102,28 +115,18 @@ export default async function handler(req, res) { // Execute all queries in parallel for better performance const [studentsData, gradesData, attendanceData, courseAndBatchNameData, remarksAndCommenterData] = await Promise.all([ - executeQuery({ - query: studentsQuery, - values: [batch_id], - }), - executeQuery({ - query: gradesQuery, - values: [batch_id], - }), - executeQuery({ - query: attendanceQuery, - values: [batch_id], - }), - executeQuery({ - query: courseAndBatchNameQuery, - values: [batch_id], - }), - executeQuery({ - query: remarksAndCommentersQuery, - values: [batch_id], - }), + prisma.$queryRawUnsafe(studentsQuery, batchId), + prisma.$queryRawUnsafe(gradesQuery, batchId), + prisma.$queryRawUnsafe(attendanceQuery, batchId), + prisma.$queryRawUnsafe(courseAndBatchNameQuery, batchId), + prisma.$queryRawUnsafe(remarksAndCommentersQuery, batchId), ]); + const serializedStudentsData = studentsData.map(serializeBatchDetailRow); + const serializedGradesData = gradesData.map(serializeBatchDetailRow); + const serializedCourseAndBatchNameData = courseAndBatchNameData.map(serializeBatchDetailRow); + const serializedRemarksAndCommenterData = remarksAndCommenterData.map(serializeBatchDetailRow); + let attendance: Array<{ student_id: unknown; date: string; is_present: number }> = []; if (attendanceData.length > 0 && studentsData.length > 0) { const { coursestart, courseend, coursedays } = attendanceData[0]; @@ -140,7 +143,7 @@ export default async function handler(req, res) { }); // Generate attendance records efficiently - studentsData.forEach((student) => { + serializedStudentsData.forEach((student) => { dateArray.forEach((date) => { // Include all dates (past, present, and future) for admin view // Attendance calculation will filter to today in frontend @@ -155,20 +158,20 @@ export default async function handler(req, res) { } res.status(200).json({ - students: studentsData, - grades: gradesData, + students: serializedStudentsData, + grades: serializedGradesData, attendance: attendance, - coursename: courseAndBatchNameData[0].coursename, - batch: courseAndBatchNameData[0].batch, - instructor: courseAndBatchNameData[0].instructor, - PM: courseAndBatchNameData[0].PM, // ADD THIS - TA: courseAndBatchNameData[0].TA, - currency: courseAndBatchNameData[0].currency, - dataentry: courseAndBatchNameData[0].dataentry, - coursedays: courseAndBatchNameData[0].coursedays, - coursestart: courseAndBatchNameData[0].coursestart, - courseend: courseAndBatchNameData[0].courseend, - remarksAndCommenterData, + coursename: serializedCourseAndBatchNameData[0].coursename, + batch: serializedCourseAndBatchNameData[0].batch, + instructor: serializedCourseAndBatchNameData[0].instructor, + PM: serializedCourseAndBatchNameData[0].PM, // ADD THIS + TA: serializedCourseAndBatchNameData[0].TA, + currency: serializedCourseAndBatchNameData[0].currency, + dataentry: serializedCourseAndBatchNameData[0].dataentry, + coursedays: serializedCourseAndBatchNameData[0].coursedays, + coursestart: serializedCourseAndBatchNameData[0].coursestart, + courseend: serializedCourseAndBatchNameData[0].courseend, + remarksAndCommenterData: serializedRemarksAndCommenterData, }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/pages/api/getbatchesdata.ts b/pages/api/getbatchesdata.ts index e288a75d..40b5f2c0 100644 --- a/pages/api/getbatchesdata.ts +++ b/pages/api/getbatchesdata.ts @@ -1,8 +1,15 @@ // FILE CONTENTS: MySQL Courses table query -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { normalizeBatchDates } from "@/utils/date-normalizers"; +type BatchReportRow = Record; + +const serializeBatchReportRow = (row: BatchReportRow): Record => + Object.fromEntries( + Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? Number(value) : value]) + ); + export default async function handler(req, res) { try { const role = String(req.query.userRole || "") @@ -213,8 +220,8 @@ export default async function handler(req, res) { query += " ORDER BY STR_TO_DATE(coursestart, '%Y-%m-%d') DESC"; - const data = await executeQuery({ query, values }); - const normalized = normalizeBatchDates(Array.from(data)); + const data = await prisma.$queryRawUnsafe(query, ...values); + const normalized = normalizeBatchDates(data.map(serializeBatchReportRow)); res.status(200).json({ batches: normalized }); } catch (error) { res.status(500).json({ error: error.message }); diff --git a/pages/api/getbatchesreport.ts b/pages/api/getbatchesreport.ts index 62433947..6f48fc9d 100644 --- a/pages/api/getbatchesreport.ts +++ b/pages/api/getbatchesreport.ts @@ -1,4 +1,11 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; + +type BatchReportRow = Record; + +const serializeBatchReportRow = (row: BatchReportRow): Record => + Object.fromEntries( + Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? Number(value) : value]) + ); export default async function handler(req, res) { if (req.method !== "POST") { @@ -31,12 +38,9 @@ export default async function handler(req, res) { ORDER BY b.coursestart DESC `; - const data = await executeQuery({ - query, - values: [startDate, endDate], - }); + const data = await prisma.$queryRawUnsafe(query, startDate, endDate); - res.status(200).json({ batches: data }); + res.status(200).json({ batches: data.map(serializeBatchReportRow) }); } catch (error) { console.error("Error:", error); res.status(500).json({ error: "Internal server error" }); diff --git a/pages/api/getcoursesdata-test.ts b/pages/api/getcoursesdata-test.ts index 44780eb8..fbfa39a5 100644 --- a/pages/api/getcoursesdata-test.ts +++ b/pages/api/getcoursesdata-test.ts @@ -1,22 +1,10 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(_req, res) { try { - /* ---------------- DATA MODIFICATION SECTION --------------- */ - const query = "SELECT course FROM vacourses"; - // const query = SELECT JSON_ARRAYAGG(JSON_OBJECT('course', course)) from vacourses; - // const query = SELECT CONCAT('[', GROUP_CONCAT(JSON_OBJECT('course', course)),']') FROM vacourses; - // const query selects a JSON object containing the course field. - // const query = "SELECT json_object('course', course) FROM vacourses"; - - const values: unknown[] = []; - const data = await executeQuery({ - query, - values, - }); - res.status(200).json({ courses: data }); - // res.send(data); // TRY + const courses = await prisma.vacourses.findMany({ select: { course: true } }); + res.status(200).json({ courses }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/getcoursesdata.ts b/pages/api/getcoursesdata.ts index 9f29d375..a6823a77 100644 --- a/pages/api/getcoursesdata.ts +++ b/pages/api/getcoursesdata.ts @@ -1,17 +1,18 @@ -import { executeQuery } from "@/lib/db"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(_req: NextApiRequest, res: NextApiResponse) { // Set cache headers to improve performance // Courses data changes infrequently, so longer cache than students res.setHeader("Cache-Control", "private, max-age=300, stale-while-revalidate=600"); try { - const query = "SELECT id, course, description, duration, duration_type FROM vacourses ORDER BY course ASC"; - - const values: unknown[] = []; - const data = await executeQuery({ query, values }); - res.status(200).json({ courses: data }); + const courses = await prisma.vacourses.findMany({ + select: { id: true, course: true, description: true, duration: true, duration_type: true }, + orderBy: { course: "asc" }, + }); + res.status(200).json({ courses }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/getdocumentsfee.ts b/pages/api/getdocumentsfee.ts index 002020f7..1688915e 100644 --- a/pages/api/getdocumentsfee.ts +++ b/pages/api/getdocumentsfee.ts @@ -1,51 +1,55 @@ -import { executeQuery } from "@/lib/db"; +import { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; + +type DocumentRow = { + id: number; + name: string | null; + id_proof: string; + disability_cert: string; + photo: string; + bank_details: string; +}; export default async function handler(req, res) { const { batch_id } = req.body; + const batchId = Number(batch_id); - try { - const documentsQuery = ` - SELECT s.id, s.name, s.id_proof, s.disability_cert, s.photo, s.bank_details - FROM vastudents s - JOIN vastudent_to_batch sb ON s.id = sb.student_id - WHERE sb.batch_id = ? - `; - - const feesQuery = ` - SELECT student_id, fee_paid, amount_1, amount_2, amount_3, nature_of_fee - FROM va_fees - WHERE batch_id = ? - `; - - const courseAndBatchNameQuery = ` - SELECT coursename, batch - FROM vabatches - WHERE id = ? - `; + if (!Number.isInteger(batchId) || batchId <= 0) { + return res.status(400).json({ error: "Invalid batch_id" }); + } - // Execute all queries in parallel for better performance + try { const [documentsData, feesData, courseAndBatchNameData] = await Promise.all([ - executeQuery({ - query: documentsQuery, - values: [batch_id], - }), - executeQuery({ - query: feesQuery, - values: [batch_id], - }), - executeQuery({ - query: courseAndBatchNameQuery, - values: [batch_id], + // Legacy rows can contain '' in these columns even though the current Prisma + // schema models valid values as YesNo. Keep this projection typed and + // parameterized while those legacy values are still present. + prisma.$queryRaw(Prisma.sql` + SELECT s.id, s.name, s.id_proof, s.disability_cert, s.photo, s.bank_details + FROM vastudents s + JOIN vastudent_to_batch sb ON sb.student_id = s.id + WHERE sb.batch_id = ${batchId} + `), + prisma.va_fees.findMany({ + where: { batch_id: batchId }, + select: { + student_id: true, + fee_paid: true, + amount_1: true, + amount_2: true, + amount_3: true, + nature_of_fee: true, + }, }), + prisma.vabatches.findUnique({ where: { id: batchId }, select: { coursename: true, batch: true } }), ]); res.status(200).json({ documents: documentsData, fees: feesData, - coursename: courseAndBatchNameData[0].coursename, - batch: courseAndBatchNameData[0].batch, + coursename: courseAndBatchNameData?.coursename ?? null, + batch: courseAndBatchNameData?.batch ?? null, }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/getstudent.ts b/pages/api/getstudent.ts index f8edf4f3..a7b5d095 100644 --- a/pages/api/getstudent.ts +++ b/pages/api/getstudent.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method !== "POST") { @@ -7,50 +7,44 @@ export default async function handler(req, res) { try { const { id, name, gender, dob, phone_number } = req.body; - const conditions: string[] = []; - const values: unknown[] = []; + const conditions: Array<{ + id?: number; + name?: string; + gender?: string; + age?: Date; + phone_number?: bigint; + }> = []; if (id !== null && id !== undefined && id !== "") { - conditions.push("id = ?"); - values.push(id); + conditions.push({ id: Number(id) }); } if (name !== null && name !== undefined && name !== "") { - conditions.push("name = ?"); - values.push(name); + conditions.push({ name }); } if (gender !== null && gender !== undefined && gender !== "") { - conditions.push("gender = ?"); - values.push(gender); + conditions.push({ gender }); } if (dob !== null && dob !== undefined && dob !== "") { - conditions.push("DATE(age) = ?"); - values.push(dob); + conditions.push({ age: new Date(`${dob}T00:00:00.000Z`) }); } if (phone_number !== null && phone_number !== undefined && phone_number !== "") { - conditions.push("phone_number = ?"); - values.push(phone_number); + conditions.push({ phone_number: BigInt(phone_number) }); } if (conditions.length === 0) { return res.status(400).json({ message: "At least one valid search criteria is required" }); } - const whereClause = conditions.join(" AND "); + const rawStudent = await prisma.vastudents.findFirst({ where: { AND: conditions } }); - let result = await executeQuery({ - query: `SELECT * FROM vastudents WHERE ${whereClause} LIMIT 1`, - values: values, - }); - - if (!result || result.length === 0) { + if (!rawStudent) { return res.status(404).json({ message: "Student not found" }); } - const rawStudent = result[0]; const ageValue = rawStudent.age; const dobDate = typeof ageValue === "string" || typeof ageValue === "number" || ageValue instanceof Date @@ -116,6 +110,6 @@ export default async function handler(req, res) { res.status(200).json(student); } catch (error) { - res.status(500).json({ message: "Internal server error" }); + res.status(500).json({ message: error instanceof Error ? error.message : "Internal server error" }); } } diff --git a/pages/api/getstudentdetails.ts b/pages/api/getstudentdetails.ts index ac3e489c..b7bd18b5 100644 --- a/pages/api/getstudentdetails.ts +++ b/pages/api/getstudentdetails.ts @@ -1,61 +1,62 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const { student_id } = req.body; try { - const studentsQuery = ` - SELECT - b.id, - b.coursename, - b.batch, - b.coursestart, - b.courseend, - b.instructor, - sb.completion_status, - sb.reason_for_status, - sb.certification_eligibility, - sb.grade, - sb.attendance, - sb.next_program, - s.enrollment_status AS enrollment_status, - GROUP_CONCAT(DISTINCT CONCAT(u.name, ': ', r.remarks) SEPARATOR ' || ') AS remarks - FROM vabatches b - JOIN vastudent_to_batch sb ON b.id = sb.batch_id - JOIN vastudents s ON s.id = sb.student_id - LEFT JOIN va_remarks r ON sb.id = r.vastudent_to_batch_id - LEFT JOIN vausers u ON r.user_id = u.id - WHERE sb.student_id = ? - GROUP BY - b.id, b.coursename, b.batch, b.coursestart, b.courseend, b.instructor, - sb.completion_status, sb.reason_for_status, sb.certification_eligibility, - sb.grade, sb.attendance, sb.next_program, s.enrollment_status - `; - - const studentNameQuery = ` - SELECT name, id - FROM vastudents - WHERE id = ? - `; + const student = await prisma.vastudents.findUnique({ + where: { id: student_id }, + select: { + id: true, + name: true, + enrollment_status: true, + vastudent_to_batch: { + where: { batch_id: { not: null } }, + select: { + completion_status: true, + reason_for_status: true, + certification_eligibility: true, + grade: true, + attendance: true, + next_program: true, + vabatches: { + select: { + id: true, + coursename: true, + batch: true, + coursestart: true, + courseend: true, + instructor: true, + }, + }, + va_remarks: { select: { remarks: true, vausers: { select: { name: true } } } }, + }, + }, + }, + }); - // Parallel execution of queries for better performance - const [studentsData, studentNameData] = await Promise.all([ - executeQuery({ - query: studentsQuery, - values: [student_id], - }), - executeQuery({ - query: studentNameQuery, - values: [student_id], - }), - ]); + const studentsData = + student?.vastudent_to_batch.map((enrollment) => ({ + ...enrollment.vabatches, + completion_status: enrollment.completion_status, + reason_for_status: enrollment.reason_for_status, + certification_eligibility: enrollment.certification_eligibility, + grade: enrollment.grade, + attendance: enrollment.attendance, + next_program: enrollment.next_program, + enrollment_status: student.enrollment_status, + remarks: + enrollment.va_remarks.length > 0 + ? enrollment.va_remarks.map(({ vausers, remarks }) => `${vausers.name}: ${remarks}`).join(" || ") + : null, + })) ?? []; res.status(200).json({ batches: studentsData, - name: studentNameData[0]?.name ?? null, - studentId: studentNameData[0]?.id ?? null, + name: student?.name ?? null, + studentId: student?.id ?? null, }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/getstudentsdata.ts b/pages/api/getstudentsdata.ts index 0fd2a4b2..7ea8b837 100644 --- a/pages/api/getstudentsdata.ts +++ b/pages/api/getstudentsdata.ts @@ -3,9 +3,16 @@ This function is called from students.tsx (Students link). student registration form. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { normalizeDateValue } from "@/utils/date-normalizers"; +type StudentReportRow = Record; + +const serializeStudentRow = (row: StudentReportRow): Record => + Object.fromEntries( + Object.entries(row).map(([key, value]) => [key, typeof value === "bigint" ? value.toString() : value]) + ); + const ALLOWED_CODES = new Set([ "ENROLLED", "NO_RESPONSE_SW_OFF", @@ -101,14 +108,16 @@ export default async function handler(req, res) { ORDER BY last_edited DESC; `; - let students = await executeQuery({ query }); + let students = await prisma.$queryRawUnsafe(query); - students = students.map((s) => ({ - ...s, - enrollment_status: toCode(s.enrollment_status), - age: normalizeDateValue(s.age), - registration_date: normalizeDateValue(s.registration_date), - })); + students = students.map((s) => + serializeStudentRow({ + ...s, + enrollment_status: toCode(s.enrollment_status), + age: normalizeDateValue(s.age), + registration_date: normalizeDateValue(s.registration_date), + }) + ); res.status(200).json({ students }); } catch (error) { console.error("Error fetching students data:", error); diff --git a/pages/api/getunassignedstudents.ts b/pages/api/getunassignedstudents.ts index dfe1e149..9e7d472a 100644 --- a/pages/api/getunassignedstudents.ts +++ b/pages/api/getunassignedstudents.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { // Only allow POST requests @@ -20,19 +20,9 @@ export default async function handler(req, res) { return res.status(400).json({ error: "Invalid batch_id" }); } - const studentsQuery = ` - SELECT id, name - FROM vastudents - WHERE id NOT IN ( - SELECT student_id - FROM vastudent_to_batch - WHERE batch_id = ? - ) - `; - - const studentsData = await executeQuery({ - query: studentsQuery, - values: [batchIdNum], + const studentsData = await prisma.vastudents.findMany({ + where: { vastudent_to_batch: { none: { batch_id: batchIdNum } } }, + select: { id: true, name: true }, }); res.status(200).json({ @@ -45,8 +35,8 @@ export default async function handler(req, res) { console.error("Request body:", req.body); } res.status(500).json({ - error: error.message || "Internal server error", - details: process.env.NODE_ENV === "development" ? error.stack : undefined, + error: error instanceof Error ? error.message : "Internal server error", + details: process.env.NODE_ENV === "development" && error instanceof Error ? error.stack : undefined, }); } } diff --git a/pages/api/getuserdata.ts b/pages/api/getuserdata.ts index 47da9dc7..820b60cd 100644 --- a/pages/api/getuserdata.ts +++ b/pages/api/getuserdata.ts @@ -1,6 +1,6 @@ // FILE CONTENTS: MySQL Courses table query -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { @@ -8,20 +8,11 @@ export default async function handler(req, res) { return res.status(400).json({ error: "Email is required" }); } - /* ---------------- DATA MODIFICATION SECTION --------------- */ - const querySQL = `SELECT - id, email, name, designation, joindate, mobilenumber, workbase, - supervisor, natureofjob, visualacuity, trainingprogram1, trainingprogram2, - trainingprogram3, role, isactive, action, lastlogin, employeeId, - gender, date_of_birth, contract_duration_months - FROM vausers WHERE email = ?`; - const email = req.body.email; - const valuesParams = [email]; - const data = await executeQuery({ query: querySQL, values: valuesParams }); - res.status(200).json({ users: data }); + const user = await prisma.vausers.findUnique({ where: { email } }); + res.status(200).json({ users: user ? [user] : [] }); } catch (error) { console.error("getuserdata API error:", error); - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/getusersdata.ts b/pages/api/getusersdata.ts index bfc1a13d..0eec151a 100644 --- a/pages/api/getusersdata.ts +++ b/pages/api/getusersdata.ts @@ -1,40 +1,20 @@ -import { executeQuery } from "@/lib/db"; -import { normalizeUserDates } from "@/utils/date-normalizers"; +import { prisma } from "@/lib/prisma"; +import { normalizeDateValue } from "@/utils/date-normalizers"; export default async function handler(req, res) { try { - const query = `SELECT - id, - employeeId, - email, - name, - designation, - DATE_FORMAT(joindate, '%Y-%m-%d') AS joindate, - mobilenumber, - workbase, - supervisor, - natureofjob, - visualacuity, - role, - isactive, - gender, - DATE_FORMAT(date_of_birth, '%Y-%m-%d') AS date_of_birth, - contract_duration_months, - program_project, - DATE_FORMAT(lastlogin, '%Y-%m-%d') AS lastlogin, - trainingprogram1, - trainingprogram2, - trainingprogram3 - FROM vausers - ORDER BY name ASC`; - - const data = await executeQuery({ - query, + const users = await prisma.vausers.findMany({ + orderBy: { name: "asc" }, }); - const normalized = normalizeUserDates(Array.from(data)); + const normalized = users.map((user) => ({ + ...user, + joindate: normalizeDateValue(user.joindate), + date_of_birth: normalizeDateValue(user.date_of_birth), + lastlogin: normalizeDateValue(user.lastlogin), + })); res.status(200).json({ users: normalized }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } diff --git a/pages/api/patchattendance.ts b/pages/api/patchattendance.ts index 6142cfab..d284e661 100644 --- a/pages/api/patchattendance.ts +++ b/pages/api/patchattendance.ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { executeQuery } from "@/lib/db"; +import { vastudents_enrollment_status } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; import type { IsPresentCode } from "@/utils/types/attendance"; type PatchRequestBody = { @@ -99,27 +100,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (is_present === 2) { // X(Cancelled): Update existing rows in batch, then INSERT only for students without records (NOT EXISTS) - await executeQuery({ - query: `UPDATE va_attendance - SET is_present = ? - WHERE batch_id = ? AND date = ?`, - values: [is_present, batchId, dateOnly], + const attendanceDate = new Date(dateOnly); + await prisma.va_attendance.updateMany({ + where: { batch_id: Number(batchId), date: attendanceDate }, + data: { is_present }, }); - - await executeQuery({ - query: ` - INSERT INTO va_attendance (batch_id, student_id, date, is_present) - SELECT sb.batch_id, sb.student_id, ?, ? - FROM vastudent_to_batch sb - WHERE sb.batch_id = ? - AND NOT EXISTS ( - SELECT 1 FROM va_attendance va - WHERE va.batch_id = sb.batch_id - AND va.student_id = sb.student_id - AND va.date = ? - ) - `, - values: [dateOnly, is_present, batchId, dateOnly], + const memberships = await prisma.vastudent_to_batch.findMany({ + where: { batch_id: Number(batchId) }, + select: { student_id: true }, + }); + const existing = await prisma.va_attendance.findMany({ + where: { batch_id: Number(batchId), date: attendanceDate }, + select: { student_id: true }, + }); + const existingIds = new Set(existing.map(({ student_id }) => student_id)); + await prisma.va_attendance.createMany({ + data: memberships + .filter(({ student_id }) => student_id != null && !existingIds.has(student_id)) + .map(({ student_id }) => ({ batch_id: Number(batchId), student_id, date: attendanceDate, is_present })), }); return res @@ -131,11 +129,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) console.log("[PATCHATTENDANCE] Processing dropout for student:", studentId); // Dropout: Update student's enrollment status to 'DROPOUT' in vastudents table - const enrollmentResult = await executeQuery({ - query: `UPDATE vastudents - SET enrollment_status = 'DROPOUT' - WHERE id = ?`, - values: [studentId], + const enrollmentResult = await prisma.vastudents.updateMany({ + where: { id: Number(studentId) }, + data: { enrollment_status: vastudents_enrollment_status.DROPOUT }, }); console.log("[PATCHATTENDANCE] Enrollment status update result:", enrollmentResult); @@ -144,11 +140,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // the query won't update the cell(s) for this record. // NOTE: This shouldn't be an issue for newer batches, but if any issues emerge with dropout not working well, check if // the records appears in the database. - const attendanceResult = await executeQuery({ - query: `UPDATE va_attendance - SET is_present = ? - WHERE batch_id = ? AND student_id = ? AND date >= ?`, - values: [is_present, batchId, studentId, dateOnly], + const attendanceResult = await prisma.va_attendance.updateMany({ + where: { batch_id: Number(batchId), student_id: Number(studentId), date: { gte: new Date(dateOnly) } }, + data: { is_present }, }); console.log("[PATCHATTENDANCE] Attendance update result:", attendanceResult); @@ -158,17 +152,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } // Normal P/A/H: UPDATE → (if not exists) INSERT (works without unique key constraints) - const updateRes = await executeQuery({ - query: `UPDATE va_attendance - SET is_present = ? - WHERE batch_id = ? AND student_id = ? AND date = ?`, - values: [is_present, batchId, studentId, dateOnly], + const updateRes = await prisma.va_attendance.updateMany({ + where: { batch_id: Number(batchId), student_id: Number(studentId), date: new Date(dateOnly) }, + data: { is_present }, }); - if ((updateRes?.affectedRows ?? 0) === 0) { - await executeQuery({ - query: `INSERT INTO va_attendance (batch_id, student_id, date, is_present) - VALUES (?, ?, ?, ?)`, - values: [batchId, studentId, dateOnly, is_present], + if (updateRes.count === 0) { + await prisma.va_attendance.create({ + data: { batch_id: Number(batchId), student_id: Number(studentId), date: new Date(dateOnly), is_present }, }); } diff --git a/pages/api/studentapplication.ts b/pages/api/studentapplication.ts index 75b871d4..bc1d03e4 100644 --- a/pages/api/studentapplication.ts +++ b/pages/api/studentapplication.ts @@ -3,7 +3,7 @@ This function is called from students.tsx (Students link). student registration form. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { try { @@ -14,61 +14,56 @@ export default async function handler(req, res) { // Format the age (DOB) to YYYY-MM-DD without timezone const formattedAge = body.age ? new Date(body.age).toISOString().split("T")[0] : null; - const values = [ - body.Quarter || null, - body.SNo || null, - body.Batch_ID || null, - body.Program_Name || null, - body.Trainee_ID || null, - body.name || null, - body.edu_qualifications || null, - body.phone_number || null, - body.alt_ph_num || null, - body.CityDistrict_State || null, - body.email || null, - body.gender || null, - formattedAge, - body.visual_acuity || null, - body.percent_loss || null, - body.employment_status || null, - body.Designation || null, - body.Languages_Known || null, - body.Program_ManagerCoordinator || null, - body.country || null, - body.state || null, - body.city || null, - body.disability || null, - body.edu_details || "", - body.objectives || null, - body.first_choice || null, - body.second_choice || null, - body.third_choice || null, - body.impairment_history || null, - body.source || null, - currentDate, - body.id_proof || "", - body.disability_cert || "", - body.photo || "", - body.bank_details || "", - body.completion_status || null, - body.reason_for_status || null, - body.certification_eligibility || null, - body.risk_factor || null, - body.remarks || null, - body.commenter || null, - ]; - - const result = await executeQuery({ - /*----- COLS: 42 NOT including: id -----*/ - query: - "INSERT INTO vastudents (Quarter, SNo, Batch_ID, Program_Name, Trainee_ID, name, edu_qualifications, phone_number, alt_ph_num, CityDistrict_State, email, gender, age, visual_acuity, percent_loss, employment_status, Designation, Languages_Known, Program_ManagerCoordinator, country, state, city, disability, edu_details, objectives, first_choice, second_choice, third_choice, impairment_history, source, registration_date, id_proof, disability_cert, photo, bank_details, completion_status, reason_for_status, certification_eligibility, risk_factor, remarks, commenter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - values: values, + const result = await prisma.vastudents.create({ + data: { + Quarter: body.Quarter || null, + SNo: body.SNo || null, + Batch_ID: body.Batch_ID || null, + Program_Name: body.Program_Name || null, + Trainee_ID: body.Trainee_ID || null, + name: body.name || null, + edu_qualifications: body.edu_qualifications || null, + phone_number: body.phone_number ? BigInt(body.phone_number) : null, + alt_ph_num: body.alt_ph_num ? BigInt(body.alt_ph_num) : null, + CityDistrict_State: body.CityDistrict_State || null, + email: body.email || null, + gender: body.gender || null, + age: formattedAge ? new Date(formattedAge) : null, + visual_acuity: body.visual_acuity || null, + percent_loss: body.percent_loss || null, + employment_status: body.employment_status || null, + Designation: body.Designation || null, + Languages_Known: body.Languages_Known || null, + Program_ManagerCoordinator: body.Program_ManagerCoordinator || null, + country: body.country || null, + state: body.state || null, + city: body.city || null, + disability: body.disability || "", + edu_details: body.edu_details || "", + objectives: body.objectives || null, + first_choice: body.first_choice || null, + second_choice: body.second_choice || null, + third_choice: body.third_choice || null, + impairment_history: body.impairment_history || null, + source: body.source || null, + registration_date: currentDate, + id_proof: body.id_proof || "", + disability_cert: body.disability_cert || "", + photo: body.photo || "", + bank_details: body.bank_details || "", + completion_status: body.completion_status || null, + reason_for_status: body.reason_for_status || null, + certification_eligibility: body.certification_eligibility || null, + risk_factor: body.risk_factor || null, + remarks: body.remarks || null, + commenter: body.commenter || null, + }, }); // Return a JSON success response instead of redirecting res.status(200).json({ success: true, message: "Student Registration Successful", - studentId: result.insertId, + studentId: result.id, }); } catch (error) { console.log(error); diff --git a/pages/api/telecaller-remarks/[id].ts b/pages/api/telecaller-remarks/[id].ts index 4d32597a..e4ba502b 100644 --- a/pages/api/telecaller-remarks/[id].ts +++ b/pages/api/telecaller-remarks/[id].ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; export default async function handler(req, res) { @@ -27,12 +27,8 @@ export default async function handler(req, res) { return res.status(401).json({ error: "User email not found in session" }); } - const performerData = await executeQuery({ - query: "SELECT id FROM vausers WHERE email = ?", - values: [performerEmail], - }); - - const userId = performerData?.[0]?.id ?? null; + const performer = await prisma.vausers.findUnique({ where: { email: performerEmail }, select: { id: true } }); + const userId = performer?.id ?? null; if (!userId) { return res.status(404).json({ error: "User not found" }); } @@ -41,12 +37,12 @@ export default async function handler(req, res) { // Delete a telecaller remark (only by the user who created it) try { // Check if the remark exists and belongs to the current user - const remarkResult = await executeQuery({ - query: "SELECT id, user_id FROM telecaller_remarks WHERE id = ?", - values: [parseInt(id)], + const remarkResult = await prisma.telecaller_remarks.findUnique({ + where: { id: parseInt(id) }, + select: { user_id: true }, }); - if (!remarkResult || remarkResult.length === 0) { + if (!remarkResult) { return res.status(404).json({ error: "Remark not found" }); } @@ -55,10 +51,7 @@ export default async function handler(req, res) { } // Delete the remark - await executeQuery({ - query: "DELETE FROM telecaller_remarks WHERE id = ? AND user_id = ?", - values: [parseInt(id), userId], - }); + await prisma.telecaller_remarks.deleteMany({ where: { id: parseInt(id), user_id: userId } }); return res.status(200).json({ success: true, @@ -80,12 +73,12 @@ export default async function handler(req, res) { try { // Check if the remark exists and belongs to the current user - const remarkResult = await executeQuery({ - query: "SELECT id, user_id FROM telecaller_remarks WHERE id = ?", - values: [parseInt(id)], + const remarkResult = await prisma.telecaller_remarks.findUnique({ + where: { id: parseInt(id) }, + select: { user_id: true }, }); - if (!remarkResult || remarkResult.length === 0) { + if (!remarkResult) { return res.status(404).json({ error: "Remark not found" }); } @@ -94,9 +87,9 @@ export default async function handler(req, res) { } // Update the remark - await executeQuery({ - query: "UPDATE telecaller_remarks SET remark = ?, updated_at = NOW() WHERE id = ? AND user_id = ?", - values: [remark.trim(), parseInt(id), userId], + await prisma.telecaller_remarks.updateMany({ + where: { id: parseInt(id), user_id: userId }, + data: { remark: remark.trim(), updated_at: new Date() }, }); return res.status(200).json({ diff --git a/pages/api/telecaller-remarks/index.ts b/pages/api/telecaller-remarks/index.ts index 8266cc01..ab868649 100644 --- a/pages/api/telecaller-remarks/index.ts +++ b/pages/api/telecaller-remarks/index.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; export default async function handler(req, res) { @@ -30,24 +30,23 @@ export default async function handler(req, res) { return res.status(400).json({ error: "Invalid student ID" }); } - const results = await executeQuery({ - query: ` - SELECT - tr.id, - tr.remark, - tr.created_at, - tr.updated_at, - u.name as user_name, - u.email as user_email - FROM telecaller_remarks tr - LEFT JOIN vausers u ON tr.user_id = u.id - WHERE tr.student_id = ? - ORDER BY tr.created_at DESC - `, - values: [studentId], + const results = await prisma.telecaller_remarks.findMany({ + where: { student_id: studentId }, + select: { + id: true, + remark: true, + created_at: true, + updated_at: true, + vausers: { select: { name: true, email: true } }, + }, + orderBy: { created_at: "desc" }, }); - return res.status(200).json(results || []); + return res + .status(200) + .json( + results.map(({ vausers, ...result }) => ({ ...result, user_name: vausers.name, user_email: vausers.email })) + ); } catch (error) { console.error("Error fetching telecaller remarks:", error); return res.status(500).json({ @@ -72,28 +71,20 @@ export default async function handler(req, res) { return res.status(401).json({ error: "User email not found in session" }); } - const performerData = await executeQuery({ - query: "SELECT id FROM vausers WHERE email = ?", - values: [performerEmail], - }); - - const userId = performerData?.[0]?.id ?? null; + const performer = await prisma.vausers.findUnique({ where: { email: performerEmail }, select: { id: true } }); + const userId = performer?.id ?? null; if (!userId) { return res.status(404).json({ error: "User not found" }); } // Insert the new remark - const insertResult = await executeQuery({ - query: ` - INSERT INTO telecaller_remarks (student_id, user_id, remark, created_at, updated_at) - VALUES (?, ?, ?, NOW(), NOW()) - `, - values: [parseInt(student_id), userId, remark], + const insertResult = await prisma.telecaller_remarks.create({ + data: { student_id: parseInt(student_id), user_id: userId, remark }, }); return res.status(201).json({ success: true, - id: insertResult.insertId, + id: insertResult.id, message: "Telecaller remark added successfully", }); } catch (error) { diff --git a/pages/api/updateassignment.ts b/pages/api/updateassignment.ts index 59f9ecc2..94fcdd67 100644 --- a/pages/api/updateassignment.ts +++ b/pages/api/updateassignment.ts @@ -1,23 +1,18 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { const body = req.body; const assignmentData = body.assignmentData; try { - // MySQL bind params must not be undefined; use null for SQL NULL - const values = [ - assignmentData.assignment_name, - assignmentData.assignment_type, - assignmentData.assignment_weight, - assignmentData.max_marks, - body.batchId, - assignmentData.id, - ].map((v) => (v === undefined ? null : v)); - - await executeQuery({ - query: `UPDATE va_grades SET assignment_name = ?, assignment_type = ?, assignment_weight = ?, max_marks = ? WHERE batch_id = ? AND assignment_name = ?`, - values, + await prisma.va_grades.updateMany({ + where: { batch_id: body.batchId, assignment_name: assignmentData.id }, + data: { + assignment_name: assignmentData.assignment_name, + assignment_type: assignmentData.assignment_type, + assignment_weight: assignmentData.assignment_weight, + max_marks: assignmentData.max_marks, + }, }); res.status(200).json({ success: true }); diff --git a/pages/api/updatebatchattendance.ts b/pages/api/updatebatchattendance.ts index 4e793182..9e7d564e 100644 --- a/pages/api/updatebatchattendance.ts +++ b/pages/api/updatebatchattendance.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method !== "POST") { @@ -10,9 +10,9 @@ export default async function handler(req, res) { return res.status(400).json({ success: false, message: "Missing required parameters" }); } try { - await executeQuery({ - query: "UPDATE va_attendance SET is_present = 2 WHERE batch_id = ? AND date = ?", - values: [batchId, date], + await prisma.va_attendance.updateMany({ + where: { batch_id: batchId, date: new Date(date) }, + data: { is_present: 2 }, }); res.status(200).json({ success: true, diff --git a/pages/api/updatebatches.ts b/pages/api/updatebatches.ts index 7974859c..3943256f 100644 --- a/pages/api/updatebatches.ts +++ b/pages/api/updatebatches.ts @@ -1,4 +1,17 @@ -import { executeQuery } from "@/lib/db"; +import { + vabatches_currency, + vabatches_status, + vabatches_trainingmode, + vastudents_enrollment_status, +} from "@prisma/client"; +import { prisma } from "@/lib/prisma"; + +const isBatchStatus = (value: unknown): value is vabatches_status => + typeof value === "string" && Object.values(vabatches_status).some((item) => item === value); +const isTrainingMode = (value: unknown): value is vabatches_trainingmode => + typeof value === "string" && Object.values(vabatches_trainingmode).some((item) => item === value); +const isCurrency = (value: unknown): value is vabatches_currency => + typeof value === "string" && Object.values(vabatches_currency).some((item) => item === value); export default async function handler(req, res) { if (req.method === "POST") { @@ -22,72 +35,57 @@ export default async function handler(req, res) { } = req.body; try { - const existingStatusRows = await executeQuery({ - query: "SELECT status FROM vabatches WHERE id = ?", - values: [id], - }); - const previousStatusRaw = existingStatusRows?.[0]?.status ?? null; - const previousStatus = typeof previousStatusRaw === "string" ? previousStatusRaw.trim().toUpperCase() : null; + const existingBatch = await prisma.vabatches.findUnique({ where: { id }, select: { status: true } }); + if (!existingBatch) return res.status(404).json({ error: "Batch not found" }); + const previousStatus = existingBatch.status; const normalizedStatus = typeof status === "string" && status.trim() !== "" ? status.trim().toUpperCase() : null; - const shouldClearEnrollmentStatus = normalizedStatus === "COMPLETE" && previousStatus !== "COMPLETE"; - - const query = ` - UPDATE vabatches - SET coursename = ?, batch = ?, coursestart = ?, courseend = ?, coursedays = ?, coursetimes = ?, instructor = ?, PM = ?, TA = ?, dataentry = ?, cost = ?, currency = ?, strength = ?, trainingmode = ?, status = ? - WHERE id = ?; - `; - - const values = [ - coursename, - batch, - coursestart, - courseend, - coursedays, - coursetimes, - instructor, - PM, - TA, - dataentry, - cost, - currency, - strength, - trainingmode, - normalizedStatus, - id, - ]; - await executeQuery({ - query, - values, + if (!isBatchStatus(normalizedStatus) || !isTrainingMode(trainingmode)) { + return res.status(400).json({ error: "Invalid batch status or training mode" }); + } + await prisma.vabatches.update({ + where: { id }, + data: { + coursename, + batch, + coursestart, + courseend, + coursedays, + coursetimes, + instructor, + PM, + TA, + dataentry, + cost, + currency: isCurrency(currency) ? currency : null, + strength, + trainingmode, + status: normalizedStatus, + }, }); - if (shouldClearEnrollmentStatus) { - // Get all student IDs in this batch first, then selectively clear enrollment_status - const studentBatchRows = await executeQuery({ - query: "SELECT student_id FROM vastudent_to_batch WHERE batch_id = ?", - values: [id], + if (normalizedStatus === vabatches_status.COMPLETE && previousStatus !== vabatches_status.COMPLETE) { + const memberships = await prisma.vastudent_to_batch.findMany({ + where: { batch_id: id }, + select: { student_id: true }, }); - - if (studentBatchRows && studentBatchRows.length > 0) { - const studentIds = studentBatchRows.map((row) => row.student_id); - const placeholders = studentIds.map(() => "?").join(","); - await executeQuery({ - query: ` - UPDATE vastudents AS s - SET s.enrollment_status = 'AVAILABLE' - WHERE s.enrollment_status = 'ENROLLED' - AND s.id IN (${placeholders}) - AND NOT EXISTS ( - SELECT 1 - FROM vastudent_to_batch AS sb2 - JOIN vabatches AS b2 ON b2.id = sb2.batch_id - WHERE sb2.student_id = s.id - AND (b2.courseend IS NULL OR b2.courseend >= CURDATE()) - AND (b2.status IS NULL OR UPPER(b2.status) <> 'COMPLETE') - ) - `, - values: studentIds, + for (const { student_id } of memberships) { + if (student_id == null) continue; + const activeBatch = await prisma.vastudent_to_batch.findFirst({ + where: { + student_id, + vabatches: { + status: { not: vabatches_status.COMPLETE }, + courseend: { gte: new Date().toISOString().slice(0, 10) }, + }, + }, }); + if (!activeBatch) { + await prisma.vastudents.updateMany({ + where: { id: student_id, enrollment_status: vastudents_enrollment_status.ENROLLED }, + data: { enrollment_status: vastudents_enrollment_status.AVAILABLE }, + }); + } } } diff --git a/pages/api/updatebatchstatus.ts b/pages/api/updatebatchstatus.ts index d7c581f1..42c50901 100644 --- a/pages/api/updatebatchstatus.ts +++ b/pages/api/updatebatchstatus.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method === "POST") { @@ -7,26 +7,22 @@ export default async function handler(req, res) { return res.status(400).json({ success: false, message: "Student ID is required." }); } try { - const result = await executeQuery({ - query: - "UPDATE vastudent_to_batch SET completion_status = ?, reason_for_status = ?, certification_eligibility = ?,next_program = ?, counseling_status = ?, placement_status = ?, placement_remarks = ? WHERE student_id = ? AND batch_id = ?", - values: [ - student.completion_status, - student.reason_for_status, - student.certification_eligibility, - student.next_program, - student.counseling_status, - student.placement_status, - student.placement_remarks, - student.id, - student.batchId, - ], - }); - await executeQuery({ - query: "UPDATE vastudents SET risk_factor = ? WHERE id = ?", - values: [student.risk_factor, student.id], - }); - if (result.affectedRows === 0) { + const [result] = await prisma.$transaction([ + prisma.vastudent_to_batch.updateMany({ + where: { student_id: student.id, batch_id: student.batchId }, + data: { + completion_status: student.completion_status, + reason_for_status: student.reason_for_status, + certification_eligibility: student.certification_eligibility, + next_program: student.next_program, + counseling_status: student.counseling_status, + placement_status: student.placement_status, + placement_remarks: student.placement_remarks, + }, + }), + prisma.vastudents.update({ where: { id: student.id }, data: { risk_factor: student.risk_factor } }), + ]); + if (result.count === 0) { return res.status(404).json({ success: false, message: `Student id: ${student.id} not found`, diff --git a/pages/api/updatecourse.ts b/pages/api/updatecourse.ts index 20d530c2..3cbabe11 100644 --- a/pages/api/updatecourse.ts +++ b/pages/api/updatecourse.ts @@ -1,25 +1,50 @@ -import { executeQuery } from "@/lib/db"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { vacourses_duration_type } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; -export default async function handler(req, res) { +export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") { - const { id, course, description, duration, duration_type } = req.body; + const { + id, + course, + description, + duration, + duration_type, + }: { + id?: unknown; + course?: unknown; + description?: unknown; + duration?: unknown; + duration_type?: unknown; + } = req.body; + + const isDurationType = (value: unknown): value is vacourses_duration_type => + typeof value === "string" && Object.values(vacourses_duration_type).some((item) => item === value); try { - const query = ` - UPDATE vacourses - SET course = ?, description = ?, duration = ?, duration_type = ? - WHERE id = ?; - `; + if ( + typeof id !== "number" || + typeof course !== "string" || + typeof duration !== "string" || + typeof duration_type !== "string" || + !isDurationType(duration_type) + ) { + return res.status(400).json({ message: "Invalid course data" }); + } - const values = [course, description, duration, duration_type, id]; - await executeQuery({ - query, - values, + await prisma.vacourses.update({ + where: { id }, + data: { + course, + description: typeof description === "string" ? description : null, + duration, + duration_type, + }, }); res.status(200).json({ message: "Course updated successfully" }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } else { res.status(400).json({ message: "Invalid request method" }); diff --git a/pages/api/updatedocumentsfee.ts b/pages/api/updatedocumentsfee.ts index bb0741b7..b2173eba 100644 --- a/pages/api/updatedocumentsfee.ts +++ b/pages/api/updatedocumentsfee.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method === "POST") { @@ -15,27 +15,19 @@ export default async function handler(req, res) { nature_of_fee, } = req.body.studentData; try { - const query = ` - UPDATE vastudents - SET id_proof = ?, disability_cert = ?, photo = ?, bank_details = ? - WHERE id = ?; - `; - - const values = [id_proof, disability_cert, photo, bank_details, id]; - await executeQuery({ - query, - values, - }); - - await executeQuery({ - query: - "UPDATE va_fees SET fee_paid = ?, amount_1 = ?, amount_2 = ?, amount_3 = ?, nature_of_fee = ? WHERE batch_id = ? AND student_id = ?", - values: [fee_paid, amount_1, amount_2, amount_3, nature_of_fee, req.body.batchId, id], - }); - + await prisma.$transaction([ + prisma.vastudents.update({ + where: { id }, + data: { id_proof, disability_cert, photo, bank_details }, + }), + prisma.va_fees.updateMany({ + where: { batch_id: req.body.batchId, student_id: id }, + data: { fee_paid, amount_1, amount_2, amount_3, nature_of_fee }, + }), + ]); res.status(200).json({ message: "Student updated successfully" }); } catch (error) { - res.status(500).json({ error: error.message }); + res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } else { res.status(400).json({ message: "Invalid request method" }); diff --git a/pages/api/updategrade.ts b/pages/api/updategrade.ts index 16409498..0aacc68e 100644 --- a/pages/api/updategrade.ts +++ b/pages/api/updategrade.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function handler(req, res) { if (req.method === "PATCH") { @@ -14,19 +14,19 @@ export default async function handler(req, res) { try { // Get current grade and max marks - const rows = await executeQuery({ - query: "SELECT grade, max_marks FROM va_grades WHERE batch_id = ? AND student_id = ? AND assignment_name = ?", - values: [batchId, studentId, assignmentName], + const row = await prisma.va_grades.findFirst({ + where: { batch_id: batchId, student_id: studentId, assignment_name: assignmentName }, + select: { grade: true, max_marks: true }, }); - if (rows.length === 0) { + if (!row) { return res.status(404).json({ success: false, message: `Grade record not found for batchId: ${batchId}, studentId: ${studentId}, assignment: ${assignmentName}`, }); } - const { grade, max_marks } = rows[0]; + const { grade, max_marks } = row; const maxMarks = typeof max_marks === "number" ? max_marks : Number(max_marks); // Validate grade range @@ -39,9 +39,9 @@ export default async function handler(req, res) { // Only update if the grade has actually changed if (grade !== newGrade) { - await executeQuery({ - query: "UPDATE va_grades SET grade = ? WHERE batch_id = ? AND student_id = ? AND assignment_name = ?", - values: [newGrade, batchId, studentId, assignmentName], + await prisma.va_grades.updateMany({ + where: { batch_id: batchId, student_id: studentId, assignment_name: assignmentName }, + data: { grade: newGrade }, }); } diff --git a/pages/api/updatestudents.ts b/pages/api/updatestudents.ts index db3bb696..be9c1fa4 100644 --- a/pages/api/updatestudents.ts +++ b/pages/api/updatestudents.ts @@ -1,5 +1,6 @@ // pages/api/updatestudents.ts -import { executeQuery } from "@/lib/db"; +import type { Prisma } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; const ALLOWED_ENROLLMENT = new Set([ null, @@ -68,14 +69,12 @@ export default async function handler(req, res) { return res.status(400).json({ message: "Student ID is required for update" }); } - const setClauses: string[] = []; - const values: unknown[] = []; + const data: Prisma.vastudentsUpdateInput = {}; // Only allow YYYY-MM-DD for age (others are null) if (Object.prototype.hasOwnProperty.call(body, "age")) { const age = typeof body.age === "string" && /^\d{4}-\d{2}-\d{2}$/.test(body.age) ? body.age : null; - setClauses.push("age = ?"); - values.push(age); + Object.assign(data, { age: age ? new Date(age) : null }); } // Only allow YYYY-MM-DD for registration_date (others are null) @@ -84,8 +83,7 @@ export default async function handler(req, res) { typeof body.registration_date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(body.registration_date) ? body.registration_date : null; - setClauses.push("registration_date = ?"); - values.push(rd); + Object.assign(data, { registration_date: rd ? new Date(rd) : null }); } for (const key of WHITELIST) { @@ -100,27 +98,18 @@ export default async function handler(req, res) { if (v && !ALLOWED_ENROLLMENT.has(v)) { return res.status(400).json({ message: "Invalid enrollment_status" }); } - setClauses.push("enrollment_status = ?"); - values.push(v); + Object.assign(data, { enrollment_status: v }); } else { const raw = body[key] ?? null; - setClauses.push(`${key} = ?`); - values.push(typeof raw === "string" ? raw.trim() : raw); + Object.assign(data, { [key]: typeof raw === "string" ? raw.trim() : raw }); } } - if (setClauses.length === 0) { + if (Object.keys(data).length === 0) { return res.status(400).json({ message: "No fields to update" }); } - // Debug guard (put this in during development to find the cause immediately) (undefined detected in values) - if (values.some((v) => v === undefined)) { - return res.status(400).json({ message: "Undefined detected in values" }); - } - - values.push(studentId); - const firstUpdateQuery = `UPDATE vastudents SET ${setClauses.join(", ")} WHERE id = ?`; - await executeQuery({ query: firstUpdateQuery, values }); + await prisma.vastudents.update({ where: { id: studentId }, data }); return res.status(200).json({ success: true, message: "Student updated successfully" }); } catch (error) { diff --git a/pages/api/updateusers.ts b/pages/api/updateusers.ts index 9486095b..39fa22c8 100644 --- a/pages/api/updateusers.ts +++ b/pages/api/updateusers.ts @@ -1,4 +1,5 @@ -import { executeQuery } from "@/lib/db"; +import { vausers_isactive } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; import { staffAuditLogger } from "../../utils/auditLogger"; @@ -8,14 +9,13 @@ const toIntOrNull = (v) => (v === undefined || v === "" || v === null || Number. // safe isactive handling function (safe isactive handling function) const normalizeIsActive = (input) => { if (input == null || input === undefined || input === "") return undefined; // do not pass if no pass (do not pass if no pass) - // Handle both "A" and "IA" (DB uses "IA" for inactive) + // Accept legacy "IA" input while storing the Prisma enum value "I". if (input === "A" || input === "IA" || input === "I") { - // Convert "I" to "IA" for DB compatibility - return input === "I" ? "IA" : input; + return input === "A" ? "A" : "I"; } // allow common form values (checkbox/toggle) defensively (allow common form values (checkbox/toggle) defensively) if (input === true || input === "true" || input === 1 || input === "1") return "A"; - if (input === false || input === "false" || input === 0 || input === "0") return "IA"; + if (input === false || input === "false" || input === 0 || input === "0") return "I"; return undefined; // if invalid value, ignore (ignore) }; // STR_TO_DATE is guaranteed to return NULL if the argument is NULL @@ -54,6 +54,13 @@ const toISODateOrNull = (v) => { return null; }; +const toDateOrNull = (v) => { + const iso = toISODateOrNull(v); + if (!iso) return null; + const date = new Date(`${iso}T00:00:00.000Z`); + return Number.isNaN(date.getTime()) ? null : date; +}; + export default async function handler(req, res) { if (req.method !== "POST") return res.status(405).json({ message: "Method Not Allowed" }); try { @@ -94,84 +101,43 @@ export default async function handler(req, res) { // Find performer (account may not exist in session) const performerEmail = session?.user?.email || null; - const performerData = performerEmail - ? await executeQuery({ query: "SELECT id FROM vausers WHERE email = ?", values: [performerEmail] }) - : []; - const performerId = performerData?.[0]?.id ?? null; + const performer = performerEmail + ? await prisma.vausers.findUnique({ where: { email: performerEmail }, select: { id: true } }) + : null; + const performerId = performer?.id ?? null; // Get previous data - const previousRows = await executeQuery({ query: "SELECT * FROM vausers WHERE id = ?", values: [idNum] }); - const previous = previousRows?.[0] ?? null; + const previous = await prisma.vausers.findUnique({ where: { id: idNum } }); try { // only include isactive if it is explicitly passed const normalizedIsActive = normalizeIsActive(isactive); const includeIsActive = normalizedIsActive !== undefined; - // dynamically construct the query - const setClauses = [ - "email = ?", - "name = ?", - "designation = ?", - "joindate = DATE(?)", - "mobilenumber = ?", - "workbase = ?", - "supervisor = ?", - "natureofjob = ?", - "visualacuity = ?", - "trainingprogram1 = ?", - "trainingprogram2 = ?", - "trainingprogram3 = ?", - "role = ?", - "action = ?", - "lastlogin = DATE(?)", - "employeeId = ?", - "gender = ?", - "date_of_birth = DATE(?)", - "contract_duration_months = ?", - ]; - - // only add isactive to the SET clause if it is explicitly passed - if (includeIsActive) { - setClauses.splice(13, 0, "isactive = ?"); // insert after role (insert after role) - } - - const query = `UPDATE vausers SET ${setClauses.join(", ")} WHERE id = ?;`; - - // construct the values - const values = [ - toNull(email), - toNull(name), - toNull(designation), - toISODateOrNull(joindate), - toNull(mobilenumber), - toNull(workbase), - toNull(supervisor), - toNull(natureofjob), - toNull(visualacuity), - toNull(trainingprogram1), - toNull(trainingprogram2), - toNull(trainingprogram3), - toNull(role), - ]; - - // only add isactive if it is explicitly passed - if (includeIsActive) { - values.push(normalizedIsActive); - } - - // add the remaining values - values.push( - toNull(action), - toISODateOrNull(lastlogin), - toNull(employeeId), - toNull(gender), - toISODateOrNull(date_of_birth), - toIntOrNull(contract_duration_months), - idNum - ); - - await executeQuery({ query, values }); + const updateData = { + email: toNull(email), + name: toNull(name), + designation: toNull(designation), + joindate: toDateOrNull(joindate), + mobilenumber: toNull(mobilenumber), + workbase: toNull(workbase), + supervisor: toNull(supervisor), + natureofjob: toNull(natureofjob), + visualacuity: toNull(visualacuity), + trainingprogram1: toNull(trainingprogram1), + trainingprogram2: toNull(trainingprogram2), + trainingprogram3: toNull(trainingprogram3), + role: toNull(role), + ...(includeIsActive ? { isactive: normalizedIsActive === "I" ? vausers_isactive.I : vausers_isactive.A } : {}), + action: toNull(action), + lastlogin: toDateOrNull(lastlogin), + employeeId: toNull(employeeId), + gender: toNull(gender), + date_of_birth: toDateOrNull(date_of_birth), + contract_duration_months: toIntOrNull(contract_duration_months), + }; + + await prisma.vausers.update({ where: { id: idNum }, data: updateData }); // Create change log (simple comparison; dates may differ in format) (simple comparison; dates may differ in format) if (previous && performerId) { @@ -208,7 +174,7 @@ export default async function handler(req, res) { // Handle isactive separately (compare "A" vs "IA" strings directly) if (includeIsActive) { const prevIsActive = previous?.isactive || null; - const currIsActive = normalizedIsActive === "I" ? "IA" : normalizedIsActive; // Convert "I" to "IA" for DB compatibility + const currIsActive = normalizedIsActive; if (prevIsActive !== currIsActive) { changes.isactive = { from: prevIsActive, to: currIsActive }; } diff --git a/pages/api/usercreate.ts b/pages/api/usercreate.ts index 1f67d50d..a146feaf 100644 --- a/pages/api/usercreate.ts +++ b/pages/api/usercreate.ts @@ -1,6 +1,7 @@ /* This function is called from users.tsx (Staff link). */ -import { executeQuery } from "@/lib/db"; +import { vausers_isactive } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; import { staffAuditLogger } from "../../utils/auditLogger"; @@ -28,6 +29,13 @@ const toISODateOrNull = (v) => { return null; }; +const toDateOrNull = (v) => { + const iso = toISODateOrNull(v); + if (!iso) return null; + const date = new Date(`${iso}T00:00:00.000Z`); + return Number.isNaN(date.getTime()) ? null : date; +}; + export default async function handler(req, res) { if (req.method !== "POST") return res.status(405).json({ message: "Method Not Allowed" }); try { @@ -40,48 +48,40 @@ export default async function handler(req, res) { } const performerEmail = session?.user?.email || null; - const performerData = performerEmail - ? await executeQuery({ - query: "SELECT id FROM vausers WHERE email = ?", - values: [performerEmail], - }) - : []; - - const performerId = performerData?.[0]?.id || "Unknown User"; + const performer = performerEmail + ? await prisma.vausers.findUnique({ where: { email: performerEmail }, select: { id: true } }) + : null; + const performerId = performer?.id || "Unknown User"; // Get data submitted in request body const body = req.body; - const data = await executeQuery({ - /* ---------- DATABASE MODIFICATION SECTION ------------- */ - // If timestamp is a field, use: user.createdAt.Date (not toString) - query: - "INSERT INTO vausers (employeeId, email, name, designation, joindate, mobilenumber, workbase, supervisor, natureofjob, visualacuity, trainingprogram1, trainingprogram2, trainingprogram3, role, isactive, action, date_of_birth, gender, contract_duration_months) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - values: [ - toNull(body.employeeId), - toNull(body.email) || "", - toNull(body.name) || "", - toNull(body.designation) || "", - toISODateOrNull(body.joindate), - toNull(body.mobilenumber), - toNull(body.workbase) || "", - toNull(body.supervisor) || "", - toNull(body.natureofjob) || "", - toNull(body.visualacuity), - toNull(body.trainingprogram1), - toNull(body.trainingprogram2), - toNull(body.trainingprogram3), - toNull(body.role) || "", - toNull(body.isactive) || "", - toNull(body.action) || "", - toISODateOrNull(body.date_of_birth), - toNull(body.gender), - toIntOrNull(body.contract_duration_months), - ], + const created = await prisma.vausers.create({ + data: { + employeeId: toNull(body.employeeId), + email: toNull(body.email) || "", + name: toNull(body.name) || "", + designation: toNull(body.designation) || "", + joindate: toDateOrNull(body.joindate), + mobilenumber: toNull(body.mobilenumber), + workbase: toNull(body.workbase) || "", + supervisor: toNull(body.supervisor) || "", + natureofjob: toNull(body.natureofjob) || "", + visualacuity: toNull(body.visualacuity), + trainingprogram1: toNull(body.trainingprogram1) || "", + trainingprogram2: toNull(body.trainingprogram2) || "", + trainingprogram3: toNull(body.trainingprogram3) || "", + role: toNull(body.role) || "", + isactive: toNull(body.isactive) === "I" ? vausers_isactive.I : vausers_isactive.A, + action: toNull(body.action) || "", + date_of_birth: toDateOrNull(body.date_of_birth), + gender: toNull(body.gender), + contract_duration_months: toIntOrNull(body.contract_duration_months), + }, }); try { // Log the staff creation - await staffAuditLogger.logStaffCreation(performerId, body, data.insertId); + await staffAuditLogger.logStaffCreation(performerId, body, created.id); } catch (auditError) { console.error("Audit logger threw:", auditError.message); console.error("Audit stack:", auditError.stack); @@ -91,7 +91,7 @@ export default async function handler(req, res) { return res.status(200).json({ success: true, message: "Staff created successfully", - staffId: data.insertId, + staffId: created.id, }); } diff --git a/pages/api/users.ts b/pages/api/users.ts index 2ef20df8..82cdb664 100644 --- a/pages/api/users.ts +++ b/pages/api/users.ts @@ -3,17 +3,14 @@ This function is called from the Users page (/users.tsx). It executes the query that SHOWS CURRENT USERS. */ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; export default async function assetHandler(req, res) { const { method } = req; switch (method) { case "GET": try { - const result = await executeQuery({ - query: "SELECT * FROM vausers", - values: [], - }); + const result = await prisma.vausers.findMany(); res.json(result); //.log(result); return result; diff --git a/pages/api/va-remarks/[id].ts b/pages/api/va-remarks/[id].ts index 4d0b0d79..a01841d2 100644 --- a/pages/api/va-remarks/[id].ts +++ b/pages/api/va-remarks/[id].ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; export default async function handler(req, res) { @@ -17,19 +17,19 @@ export default async function handler(req, res) { // Delete a va remark (only by the user who created it) try { // Get user ID from session - const userId = session?.user?.id; + const userId = Number(session?.user?.id); if (!userId) { return res.status(404).json({ error: "User ID not found in session" }); } // Check if the remark exists and belongs to the current user - const remarkResult = await executeQuery({ - query: "SELECT id, user_id FROM va_remarks WHERE id = ?", - values: [parseInt(id)], + const remarkResult = await prisma.va_remarks.findUnique({ + where: { id: parseInt(id) }, + select: { user_id: true }, }); - if (!remarkResult || remarkResult.length === 0) { + if (!remarkResult) { return res.status(404).json({ error: "Remark not found" }); } @@ -38,10 +38,7 @@ export default async function handler(req, res) { } // Delete the remark - await executeQuery({ - query: "DELETE FROM va_remarks WHERE id = ? AND user_id = ?", - values: [parseInt(id), userId], - }); + await prisma.va_remarks.deleteMany({ where: { id: parseInt(id), user_id: userId } }); return res.status(200).json({ success: true, @@ -63,19 +60,19 @@ export default async function handler(req, res) { try { // Get user ID from session - const userId = session?.user?.id; + const userId = Number(session?.user?.id); if (!userId) { return res.status(404).json({ error: "User ID not found in session" }); } // Check if the remark exists and belongs to the current user - const remarkResult = await executeQuery({ - query: "SELECT id, user_id FROM va_remarks WHERE id = ?", - values: [parseInt(id)], + const remarkResult = await prisma.va_remarks.findUnique({ + where: { id: parseInt(id) }, + select: { user_id: true }, }); - if (!remarkResult || remarkResult.length === 0) { + if (!remarkResult) { return res.status(404).json({ error: "Remark not found" }); } @@ -84,9 +81,9 @@ export default async function handler(req, res) { } // Update the remark - await executeQuery({ - query: "UPDATE va_remarks SET remarks = ? WHERE id = ? AND user_id = ?", - values: [remarks.trim(), parseInt(id), userId], + await prisma.va_remarks.updateMany({ + where: { id: parseInt(id), user_id: userId }, + data: { remarks: remarks.trim() }, }); return res.status(200).json({ diff --git a/pages/api/va-remarks/index.ts b/pages/api/va-remarks/index.ts index ec10ba58..b801b1f8 100644 --- a/pages/api/va-remarks/index.ts +++ b/pages/api/va-remarks/index.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getServerSession } from "@/lib/server-auth"; export default async function handler(req, res) { @@ -17,24 +17,19 @@ export default async function handler(req, res) { } try { - const results = await executeQuery({ - query: ` - SELECT - vr.id, - vr.remarks, - vr.user_id, - u.name as user_name, - u.email as user_email - FROM va_remarks vr - JOIN vastudent_to_batch stb ON vr.vastudent_to_batch_id = stb.id - JOIN vausers u ON vr.user_id = u.id - WHERE stb.student_id = ? AND stb.batch_id = ? - ORDER BY vr.id DESC - `, - values: [parseInt(student_id), parseInt(batch_id)], + const results = await prisma.va_remarks.findMany({ + where: { + vastudent_to_batch: { student_id: parseInt(student_id), batch_id: parseInt(batch_id) }, + }, + select: { id: true, remarks: true, user_id: true, vausers: { select: { name: true, email: true } } }, + orderBy: { id: "desc" }, }); - return res.status(200).json(results); + return res + .status(200) + .json( + results.map(({ vausers, ...remark }) => ({ ...remark, user_name: vausers.name, user_email: vausers.email })) + ); } catch (error) { console.error("Error fetching va remarks:", error); return res.status(500).json({ error: "Failed to fetch va remarks" }); @@ -58,29 +53,25 @@ export default async function handler(req, res) { } // First, get the vastudent_to_batch_id - const studentToBatchResult = await executeQuery({ - query: "SELECT id FROM vastudent_to_batch WHERE student_id = ? AND batch_id = ?", - values: [parseInt(student_id), parseInt(batch_id)], + const studentToBatchResult = await prisma.vastudent_to_batch.findFirst({ + where: { student_id: parseInt(student_id), batch_id: parseInt(batch_id) }, + select: { id: true }, }); - if (!studentToBatchResult || studentToBatchResult.length === 0) { + if (!studentToBatchResult) { return res.status(404).json({ error: "Student not found in this batch" }); } - const vastudentToBatchId = studentToBatchResult[0].id; + const vastudentToBatchId = studentToBatchResult.id; // Insert the new remark - const insertResult = await executeQuery({ - query: ` - INSERT INTO va_remarks (vastudent_to_batch_id, remarks, user_id) - VALUES (?, ?, ?) - `, - values: [vastudentToBatchId, remarks, userId], + const insertResult = await prisma.va_remarks.create({ + data: { vastudent_to_batch_id: vastudentToBatchId, remarks, user_id: Number(userId) }, }); return res.status(201).json({ success: true, - id: insertResult.insertId, + id: insertResult.id, message: "VA remark added successfully", }); } catch (error) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69bc94d8..23e783ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,10 @@ importers: dependencies: '@better-auth/passkey': specifier: ^1.7.1 - version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.9.8)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(better-call@1.4.0(zod@4.4.3))(nanostores@1.5.1) + version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.15.3)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(better-call@1.4.0(zod@4.4.3))(nanostores@1.5.1) + '@better-auth/prisma-adapter': + specifier: 1.7.1 + version: 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2)) '@emotion/react': specifier: 11.14.0 version: 11.14.0(@types/react@18.3.24)(react@18.2.0) @@ -29,6 +32,9 @@ importers: '@mui/styled-engine-sc': specifier: 7.3.2 version: 7.3.2(@types/react@18.3.24)(styled-components@6.1.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) + '@prisma/adapter-mariadb': + specifier: 7.10.0 + version: 7.10.0 '@prisma/client': specifier: 7.10.0 version: 7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2) @@ -43,19 +49,19 @@ importers: version: 32.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) better-auth: specifier: ^1.7.1 - version: 1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.9.8)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.15.3)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) date-fns: specifier: 4.1.0 version: 4.1.0 + mariadb: + specifier: ^3.5.4 + version: 3.5.4 moment: specifier: 2.30.1 version: 2.30.1 moment-timezone: specifier: ^0.5.48 version: 0.5.48 - mysql2: - specifier: 3.9.8 - version: 3.9.8 next: specifier: ^14.2.33 version: 14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1481,6 +1487,9 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/adapter-mariadb@7.10.0': + resolution: {integrity: sha512-n/mNi6/l2V79pNwuvzRlZO+X8dr38eqjx1norvXs3t+bTJkSotxwV40HW0SCSl2vBp/YMfX2w6ZHKePgurf1gw==} + '@prisma/client-runtime-utils@7.10.0': resolution: {integrity: sha512-cnCy7lUV8/CctgKVEmqAbSLAmwqJdE/qAlqTBk/0NDk59zEb2cZ0M0M0E4vVPnqbSEYudRroQDvOWfUZH6RIfw==} @@ -1508,6 +1517,9 @@ packages: '@prisma/dev@0.24.17': resolution: {integrity: sha512-UvdZzmpFwknnfreh6Jije84ekkYGPYEJhXG1tFzCsCfQyzJifrOo38eZc0qajzvaC6OLUOrN9ML5XfCnEZL9DA==} + '@prisma/driver-adapter-utils@7.10.0': + resolution: {integrity: sha512-u8zkcRLlaryO652T4qavBg0HmzNW5tSKdsCn6hc1PhWAp/J6k0vrxLuUs+b9o+HcjsK7Dfa01o4OFSn0frauJA==} + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': resolution: {integrity: sha512-8OJ6RuZTZ06eFUOtBwxVmv8XMmOW6HWN5F+uxUbZkGxR0Bfab1dfAdXaHPmR5mb59E+fmUo8IOzXlbLY1SClbw==} @@ -4034,6 +4046,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4041,10 +4057,6 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lru-cache@8.0.5: - resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} - engines: {node: '>=16.14'} - lru.min@1.1.5: resolution: {integrity: sha512-5J9ysMYUpYIg9RF2vJpy9SinEmSviFSe0GyPpCQ4L5QSkLAgeLXlTAOu2ZwWUU5m+0SBl6gUU1R1ZQB3aKypfA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} @@ -4067,6 +4079,14 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + mariadb@3.4.5: + resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==} + engines: {node: '>= 14'} + + mariadb@3.5.4: + resolution: {integrity: sha512-3m0vdfgRkjle5/9hHl4R7rlVvnDA7QfuFSNkdIAQNROWKOS0J3b9IQK9HaZSCGEVKiDUbFXFiINfC+cmR5P/aA==} + engines: {node: '>= 20.0.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4140,10 +4160,6 @@ packages: resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} engines: {node: '>= 8.0'} - mysql2@3.9.8: - resolution: {integrity: sha512-+5JKNjPuks1FNMoy9TYpl77f+5frbTklz7eb3XDwbpsERRLEeXiW2PDEkakYF50UuKU2qwfGnyXpKYvukv8mGA==} - engines: {node: '>= 8.0'} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -6351,14 +6367,14 @@ snapshots: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1) '@better-auth/utils': 0.4.2 - '@better-auth/passkey@1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.9.8)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(better-call@1.4.0(zod@4.4.3))(nanostores@1.5.1)': + '@better-auth/passkey@1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.15.3)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(better-call@1.4.0(zod@4.4.3))(nanostores@1.5.1)': dependencies: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1) '@better-auth/utils': 0.4.2 '@better-fetch/fetch': 1.3.1 '@simplewebauthn/browser': 13.3.0 '@simplewebauthn/server': 13.3.2 - better-auth: 1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.9.8)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + better-auth: 1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.15.3)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) better-call: 1.4.0(zod@4.4.3) nanostores: 1.5.1 zod: 4.4.3 @@ -7134,6 +7150,11 @@ snapshots: '@popperjs/core@2.11.8': {} + '@prisma/adapter-mariadb@7.10.0': + dependencies: + '@prisma/driver-adapter-utils': 7.10.0 + mariadb: 3.4.5 + '@prisma/client-runtime-utils@7.10.0': {} '@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2)': @@ -7176,6 +7197,10 @@ snapshots: transitivePeerDependencies: - typescript + '@prisma/driver-adapter-utils@7.10.0': + dependencies: + '@prisma/debug': 7.10.0 + '@prisma/engines-version@7.10.0-4.0edf323efd1d98336f3f0a68684b56f689b900d3': {} '@prisma/engines@7.10.0': @@ -8202,7 +8227,7 @@ snapshots: baseline-browser-mapping@2.9.15: {} - better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.9.8)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + better-auth@1.7.1(@prisma/client@7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2))(mysql2@3.15.3)(next@14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@better-auth/core': 1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1) '@better-auth/drizzle-adapter': 1.7.1(@better-auth/core@1.7.1(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.9)(kysely@0.29.5)(nanostores@1.5.1))(@better-auth/utils@0.4.2) @@ -8223,7 +8248,7 @@ snapshots: zod: 4.4.3 optionalDependencies: '@prisma/client': 7.10.0(prisma@7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2))(typescript@5.9.2) - mysql2: 3.9.8 + mysql2: 3.15.3 next: 14.2.33(@babel/core@7.28.4)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) prisma: 7.10.0(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.2) react: 18.2.0 @@ -10118,14 +10143,14 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 lru-cache@7.18.3: {} - lru-cache@8.0.5: {} - lru.min@1.1.5: {} lz-string@1.5.0: {} @@ -10146,6 +10171,22 @@ snapshots: dependencies: tmpl: 1.0.5 + mariadb@3.4.5: + dependencies: + '@types/geojson': 7946.0.16 + '@types/node': 24.3.1 + denque: 2.1.0 + iconv-lite: 0.6.3 + lru-cache: 10.4.3 + + mariadb@3.5.4: + dependencies: + '@types/geojson': 7946.0.16 + '@types/node': 24.3.1 + denque: 2.1.0 + iconv-lite: 0.7.3 + lru-cache: 11.5.2 + math-intrinsics@1.1.0: {} memoize-one@6.0.0: {} @@ -10209,17 +10250,6 @@ snapshots: seq-queue: 0.0.5 sqlstring: 2.3.3 - mysql2@3.9.8: - dependencies: - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.6.3 - long: 5.3.2 - lru-cache: 8.0.5 - named-placeholders: 1.1.3 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - mz@2.7.0: dependencies: any-promise: 1.3.0 diff --git a/utils/batch-status-rules-config.ts b/utils/batch-status-rules-config.ts index c545c020..6a58ce68 100644 --- a/utils/batch-status-rules-config.ts +++ b/utils/batch-status-rules-config.ts @@ -1,4 +1,4 @@ -import { executeQuery } from "@/lib/db"; +import { prisma } from "@/lib/prisma"; import { getRulesForCourse, mergeBatchStatusDerivedRules, @@ -14,19 +14,7 @@ type SavedRules = { defaultRules?: unknown; }; -async function ensureAppSettingsTable() { - await executeQuery({ - query: ` - CREATE TABLE IF NOT EXISTS va_app_settings ( - setting_key VARCHAR(100) NOT NULL PRIMARY KEY, - setting_value LONGTEXT NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP - ) - `, - }); -} - -function parseStoredJson(raw) { +function parseStoredJson(raw: string | null | undefined) { if (raw == null || raw === "") return null; try { const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; @@ -37,16 +25,8 @@ function parseStoredJson(raw) { } export async function getBatchStatusDerivedRules() { - await ensureAppSettingsTable(); - - const rows = await executeQuery({ - query: "SELECT setting_value FROM va_app_settings WHERE setting_key = ? LIMIT 1", - values: [SETTING_KEY], - }); - - const raw = rows?.[0]?.setting_value; - const stored = parseStoredJson(raw); - return stored; + const setting = await prisma.va_app_settings.findUnique({ where: { setting_key: SETTING_KEY } }); + return parseStoredJson(setting?.setting_value); } /** @@ -102,8 +82,6 @@ export async function saveBatchStatusDerivedRules(body): Promise { await assertCertificationLabelsInDropdown(validated.rules.certification); await assertCompletionLabelsInDropdown(validated.rules.completion); - await ensureAppSettingsTable(); - const current = await getBatchStatusDerivedRules(); const currentDefault = mergeBatchStatusDerivedRules(current?.defaultRules ?? current); const byCourse = current?.byCourse && typeof current.byCourse === "object" ? { ...current.byCourse } : {}; @@ -126,13 +104,10 @@ export async function saveBatchStatusDerivedRules(body): Promise { const json = JSON.stringify(payload); - await executeQuery({ - query: ` - INSERT INTO va_app_settings (setting_key, setting_value) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value) - `, - values: [SETTING_KEY, json], + await prisma.va_app_settings.upsert({ + where: { setting_key: SETTING_KEY }, + create: { setting_key: SETTING_KEY, setting_value: json }, + update: { setting_value: json }, }); return { diff --git a/utils/dropdown-config.ts b/utils/dropdown-config.ts index f6d25484..084c53c6 100644 --- a/utils/dropdown-config.ts +++ b/utils/dropdown-config.ts @@ -1,186 +1,137 @@ -// @ts-nocheck -import { executeQuery } from "@/lib/db"; - -const SUPPORTED_DROPDOWN_KEYS = new Set(["staff_designation", "certification_eligibility", "completion_status"]); - -const STAFF_DESIGNATION_DEFAULTS = [ - "Trainer", - "Teaching Assistant", - "Program Coordinator", - "Telecaller", - "Training Coordinator", - "Program Manager", - "Sr. Trainer", - "L & D Executive", - "Head of Training", - "Trainer plus Telecaller", -]; - -const CERTIFICATION_ELIGIBILITY_DEFAULTS = [ - "Completion Certificate", - "Participation Certificate", - "Not Eligible", - "Ineligible", -]; - -const COMPLETION_STATUS_DEFAULTS = ["Completed", "Incomplete", "Drop Out"]; - -async function ensureDropdownValuesTable() { - await executeQuery({ - query: ` - CREATE TABLE IF NOT EXISTS va_dropdown_values ( - id INT AUTO_INCREMENT PRIMARY KEY, - dropdown_key VARCHAR(100) NOT NULL, - value VARCHAR(255) NOT NULL, - label VARCHAR(255) NOT NULL, - sort_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY uniq_dropdown_value (dropdown_key, value) - ) - `, - }); -} +import { prisma } from "@/lib/prisma"; + +type DropdownKey = "staff_designation" | "certification_eligibility" | "completion_status"; + +const SUPPORTED_DROPDOWN_KEYS = new Set([ + "staff_designation", + "certification_eligibility", + "completion_status", +]); + +const DEFAULTS_BY_KEY: Record = { + staff_designation: [ + "Trainer", + "Teaching Assistant", + "Program Coordinator", + "Telecaller", + "Training Coordinator", + "Program Manager", + "Sr. Trainer", + "L & D Executive", + "Head of Training", + "Trainer plus Telecaller", + ], + certification_eligibility: ["Completion Certificate", "Participation Certificate", "Not Eligible", "Ineligible"], + completion_status: ["Completed", "Incomplete", "Drop Out"], +}; -function normalizeKey(key) { +function normalizeKey(key: unknown): DropdownKey | null { if (typeof key !== "string") return null; const normalized = key.trim(); - if (!normalized || !SUPPORTED_DROPDOWN_KEYS.has(normalized)) return null; - return normalized; + return SUPPORTED_DROPDOWN_KEYS.has(normalized as DropdownKey) ? (normalized as DropdownKey) : null; } -function normalizeNonEmptyString(v) { - if (typeof v !== "string") return null; - const s = v.trim(); - return s.length ? s : null; +function normalizeNonEmptyString(value: unknown): string | null { + if (typeof value !== "string") return null; + const normalized = value.trim(); + return normalized.length > 0 ? normalized : null; } -const DEFAULTS_BY_KEY = { - staff_designation: STAFF_DESIGNATION_DEFAULTS, - certification_eligibility: CERTIFICATION_ELIGIBILITY_DEFAULTS, - completion_status: COMPLETION_STATUS_DEFAULTS, -}; - -const seedDefaultsIfEmpty = async (dropdownKey) => { - const defaults = DEFAULTS_BY_KEY[dropdownKey]; - if (!defaults?.length) return; - - const countRes = await executeQuery({ - query: "SELECT COUNT(*) as c FROM va_dropdown_values WHERE dropdown_key = ?", - values: [dropdownKey], - }); - const count = countRes?.[0]?.c ?? 0; +async function seedDefaultsIfEmpty(dropdownKey: DropdownKey) { + const count = await prisma.va_dropdown_values.count({ where: { dropdown_key: dropdownKey } }); if (count > 0) return; - for (const opt of defaults) { - // eslint-disable-next-line no-await-in-loop - await executeQuery({ - query: ` - INSERT INTO va_dropdown_values (dropdown_key, value, label, sort_order) - VALUES (?, ?, ?, ?) - `, - values: [dropdownKey, opt, opt, 0], - }); - } -}; + await prisma.va_dropdown_values.createMany({ + data: DEFAULTS_BY_KEY[dropdownKey].map((value) => ({ + dropdown_key: dropdownKey, + value, + label: value, + sort_order: 0, + })), + skipDuplicates: true, + }); +} -const USED_VALUE_CHECKS_BY_DROPDOWN_KEY = { - staff_designation: [{ table: "vausers", column: "designation" }], - certification_eligibility: [ - { table: "vastudent_to_batch", column: "certification_eligibility" }, - { table: "vastudents", column: "certification_eligibility" }, - ], - completion_status: [ - { table: "vastudent_to_batch", column: "completion_status" }, - { table: "vastudents", column: "completion_status" }, - ], -}; +async function ensureSeeded(dropdownKey: DropdownKey) { + await seedDefaultsIfEmpty(dropdownKey); +} -export async function getDropdownOptions({ key }) { +export async function getDropdownOptions({ key }: { key: unknown }) { const dropdownKey = normalizeKey(key); if (!dropdownKey) return []; - await ensureDropdownValuesTable(); - await seedDefaultsIfEmpty(dropdownKey); - - const results = await executeQuery({ - query: ` - SELECT value - FROM va_dropdown_values - WHERE dropdown_key = ? - ORDER BY value ASC - `, - values: [dropdownKey], + await ensureSeeded(dropdownKey); + return prisma.va_dropdown_values.findMany({ + where: { dropdown_key: dropdownKey }, + select: { value: true }, + orderBy: { value: "asc" }, }); - - return results ?? []; } -export async function upsertDropdownOption({ key, value, label = value, sortOrder = 0 }) { +export async function upsertDropdownOption({ + key, + value, + label = value, + sortOrder = 0, +}: { + key: unknown; + value: unknown; + label?: unknown; + sortOrder?: number; +}) { const dropdownKey = normalizeKey(key); if (!dropdownKey) throw new Error("Unsupported dropdown key"); const normalizedValue = normalizeNonEmptyString(value); if (!normalizedValue) throw new Error("Value is required"); - await ensureDropdownValuesTable(); - await seedDefaultsIfEmpty(dropdownKey); - - await executeQuery({ - query: ` - INSERT INTO va_dropdown_values (dropdown_key, value, label, sort_order) - VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - label = VALUES(label) - `, - values: [dropdownKey, normalizedValue, label ?? normalizedValue, sortOrder], + await ensureSeeded(dropdownKey); + await prisma.va_dropdown_values.upsert({ + where: { dropdown_key_value: { dropdown_key: dropdownKey, value: normalizedValue } }, + create: { + dropdown_key: dropdownKey, + value: normalizedValue, + label: normalizeNonEmptyString(label) ?? normalizedValue, + sort_order: sortOrder, + }, + update: { label: normalizeNonEmptyString(label) ?? normalizedValue }, }); } -export async function deleteDropdownOption({ key, value }) { +export async function deleteDropdownOption({ key, value }: { key: unknown; value: unknown }) { const dropdownKey = normalizeKey(key); if (!dropdownKey) throw new Error("Unsupported dropdown key"); const normalizedValue = normalizeNonEmptyString(value); if (!normalizedValue) throw new Error("Value is required"); - await ensureDropdownValuesTable(); - await seedDefaultsIfEmpty(dropdownKey); - - const countRes = await executeQuery({ - query: "SELECT COUNT(*) as c FROM va_dropdown_values WHERE dropdown_key = ?", - values: [dropdownKey], - }); - const count = countRes?.[0]?.c ?? 0; - - // Safety: keep at least 1 configured option so required staff forms don't break. + await ensureSeeded(dropdownKey); + const count = await prisma.va_dropdown_values.count({ where: { dropdown_key: dropdownKey } }); if (count <= 1) { return { deleted: false, reason: `Cannot delete the last remaining ${dropdownKey} option.` }; } - const usedChecks = USED_VALUE_CHECKS_BY_DROPDOWN_KEY[dropdownKey]; - if (usedChecks?.length) { - for (const { table, column } of usedChecks) { - // eslint-disable-next-line no-await-in-loop - const usedRes = await executeQuery({ - query: `SELECT COUNT(*) as c FROM ${table} WHERE ${column} = ?`, - values: [normalizedValue], - }); - const usedCount = usedRes?.[0]?.c ?? 0; - if (usedCount > 0) { - const reason = - dropdownKey === "staff_designation" - ? "Cannot delete: this value is used by existing staff." - : "Cannot delete: this value is in use."; - return { deleted: false, reason }; - } - } + const usageCount = + dropdownKey === "staff_designation" + ? await prisma.vausers.count({ where: { designation: normalizedValue } }) + : dropdownKey === "certification_eligibility" + ? (await prisma.vastudent_to_batch.count({ where: { certification_eligibility: normalizedValue } })) + + (await prisma.vastudents.count({ where: { certification_eligibility: normalizedValue } })) + : (await prisma.vastudent_to_batch.count({ where: { completion_status: normalizedValue } })) + + (await prisma.vastudents.count({ where: { completion_status: normalizedValue } })); + + if (usageCount > 0) { + return { + deleted: false, + reason: + dropdownKey === "staff_designation" + ? "Cannot delete: this value is used by existing staff." + : "Cannot delete: this value is in use.", + }; } - const deleteRes = await executeQuery({ - query: "DELETE FROM va_dropdown_values WHERE dropdown_key = ? AND value = ?", - values: [dropdownKey, normalizedValue], + const result = await prisma.va_dropdown_values.deleteMany({ + where: { dropdown_key: dropdownKey, value: normalizedValue }, }); - - const deleted = (deleteRes?.affectedRows ?? 0) > 0; - return { deleted }; + return { deleted: result.count > 0 }; } From 132740e2053eeffd21bd34b82d317853be4d9845 Mon Sep 17 00:00:00 2001 From: Justin McLellan Date: Wed, 16 Sep 2026 22:53:18 -0500 Subject: [PATCH 2/2] Ensure Prisma client is generated during builds --- Dockerfile | 4 ++++ package.json | 4 +++- prisma.config.ts | 8 +------- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7e17563b..fce413c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,10 @@ COPY prisma.config.ts ./prisma.config.ts RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts --store-dir=/pnpm/store +# The install above intentionally skips lifecycle scripts, so generate the +# typed client explicitly before compiling the application. +RUN pnpm prisma generate + # Copy source code COPY . . diff --git a/package.json b/package.json index 5d2979f6..cd026c8d 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "prisma:generate": "prisma generate", "start": "next start", "lint": "next lint", "format": "prettier --check --ignore-path .gitignore .", @@ -18,7 +19,8 @@ "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", - "prepare": "husky" + "prepare": "husky", + "postinstall": "prisma generate" }, "dependencies": { "@better-auth/passkey": "^1.7.1", diff --git a/prisma.config.ts b/prisma.config.ts index fea2f031..b50c74a1 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -9,13 +9,7 @@ const databaseUrl = ? `mysql://${encodeURIComponent(MYSQL_USER)}:${encodeURIComponent(MYSQL_PASSWORD)}@${MYSQL_HOST}:${MYSQL_PORT ?? "3306"}/${encodeURIComponent(MYSQL_DATABASE)}` : undefined); -if (!databaseUrl) { - throw new Error("Set DATABASE_URL or MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, and MYSQL_PASSWORD."); -} - export default defineConfig({ schema: "prisma", - datasource: { - url: databaseUrl, - }, + ...(databaseUrl ? { datasource: { url: databaseUrl } } : {}), });