From 37b37b2355bb3db6bebbb03dea24a5777c9d0f1e Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sat, 18 Oct 2025 19:26:43 +0530 Subject: [PATCH 01/11] add db schema for forms --- .../migration.sql | 98 +++++++++++++++++++ .../20251018123047_add_form_url/migration.sql | 12 +++ backend/prisma/schema.prisma | 83 ++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 backend/prisma/migrations/20251018092621_add_forms_to_orbis/migration.sql create mode 100644 backend/prisma/migrations/20251018123047_add_form_url/migration.sql diff --git a/backend/prisma/migrations/20251018092621_add_forms_to_orbis/migration.sql b/backend/prisma/migrations/20251018092621_add_forms_to_orbis/migration.sql new file mode 100644 index 0000000..b3c1c4e --- /dev/null +++ b/backend/prisma/migrations/20251018092621_add_forms_to_orbis/migration.sql @@ -0,0 +1,98 @@ +-- CreateEnum +CREATE TYPE "FieldType" AS ENUM ('TEXT', 'NUMBER', 'EMAIL', 'MULTIPLE_CHOICE', 'CHECKBOX', 'SINGLE_CHOICE', 'FILE', 'DATE', 'STAR_RATING', 'DROPDOWN'); + +-- CreateTable +CREATE TABLE "Form" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "createdBy" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "slug" TEXT NOT NULL, + "isTemplate" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Form_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FormContributor" ( + "id" TEXT NOT NULL, + "formId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FormContributor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FormField" ( + "id" TEXT NOT NULL, + "formId" TEXT NOT NULL, + "label" TEXT NOT NULL, + "fieldType" "FieldType" NOT NULL, + "isRequired" BOOLEAN NOT NULL DEFAULT false, + "options" JSONB, + "position" INTEGER NOT NULL, + "placeholder" TEXT, + "helpText" TEXT, + "validation" JSONB, + "conditions" JSONB, + + CONSTRAINT "FormField_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FormResponse" ( + "id" TEXT NOT NULL, + "formId" TEXT NOT NULL, + "submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "submittedBy" TEXT, + "responderIp" TEXT, + "userAgent" TEXT, + "metadata" JSONB, + + CONSTRAINT "FormResponse_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FieldAnswer" ( + "id" TEXT NOT NULL, + "responseId" TEXT NOT NULL, + "fieldId" TEXT NOT NULL, + "answerValue" TEXT, + "answerJson" JSONB, + + CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Form_slug_key" ON "Form"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "FormContributor_formId_userId_key" ON "FormContributor"("formId", "userId"); + +-- AddForeignKey +ALTER TABLE "Form" ADD CONSTRAINT "Form_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FormContributor" ADD CONSTRAINT "FormContributor_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FormContributor" ADD CONSTRAINT "FormContributor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FormField" ADD CONSTRAINT "FormField_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FormResponse" ADD CONSTRAINT "FormResponse_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FormResponse" ADD CONSTRAINT "FormResponse_submittedBy_fkey" FOREIGN KEY ("submittedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/migrations/20251018123047_add_form_url/migration.sql b/backend/prisma/migrations/20251018123047_add_form_url/migration.sql new file mode 100644 index 0000000..b40f256 --- /dev/null +++ b/backend/prisma/migrations/20251018123047_add_form_url/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[formUrl]` on the table `Form` will be added. If there are existing duplicate values, this will fail. + - Added the required column `formUrl` to the `Form` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 46a1dd6..e9b7d3c 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -24,6 +24,9 @@ model User { applications Application[] createdEvents Event[] teamMembers TeamMember[] + formsCreated Form[] @relation("UserForms") + contributions FormContributor[] + formResponses FormResponse[] @relation("UserFormResponses") } model UserProfile { @@ -320,3 +323,83 @@ enum TeamMemberRole { LEADER MEMBER } + +model Form { + id String @id @default(cuid()) + title String + description String? + createdBy String + formUrl String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + isActive Boolean @default(true) + slug String @unique + isTemplate Boolean @default(false) + creator User @relation("UserForms", fields: [createdBy], references: [id]) + contributors FormContributor[] + fields FormField[] + responses FormResponse[] +} + +model FormContributor { + id String @id @default(cuid()) + formId String + userId String + addedAt DateTime @default(now()) + form Form @relation(fields: [formId], references: [id]) + user User @relation(fields: [userId], references: [id]) + @@unique([formId, userId]) +} + +model FormField { + id String @id @default(cuid()) + formId String + label String + fieldType FieldType + isRequired Boolean @default(false) + options Json? + position Int + placeholder String? + helpText String? + validation Json? + conditions Json? + form Form @relation(fields: [formId], references: [id]) + answers FieldAnswer[] +} + +enum FieldType { + TEXT + NUMBER + EMAIL + MULTIPLE_CHOICE + CHECKBOX + SINGLE_CHOICE + FILE + DATE + STAR_RATING + DROPDOWN +} + + +model FormResponse { + id String @id @default(cuid()) + formId String + submittedAt DateTime @default(now()) + submittedBy String? + responderIp String? + userAgent String? + metadata Json? + form Form @relation(fields: [formId], references: [id]) + user User? @relation("UserFormResponses", fields: [submittedBy], references: [id]) + answers FieldAnswer[] +} + +model FieldAnswer { + id String @id @default(cuid()) + responseId String + fieldId String + answerValue String? + answerJson Json? + response FormResponse @relation(fields: [responseId], references: [id]) + field FormField @relation(fields: [fieldId], references: [id]) +} \ No newline at end of file From 589fe3a7c021944309cba37efbfc7050249bb7bc Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sat, 18 Oct 2025 21:03:00 +0530 Subject: [PATCH 02/11] make changes to form permissions and add create , update , delete form controllers --- .../migration.sql | 68 ++++ backend/prisma/schema.prisma | 25 +- backend/src/controllers/form.js | 358 ++++++++++++++++++ 3 files changed, 441 insertions(+), 10 deletions(-) create mode 100644 backend/prisma/migrations/20251018152641_add_permissions_to_forms/migration.sql create mode 100644 backend/src/controllers/form.js diff --git a/backend/prisma/migrations/20251018152641_add_permissions_to_forms/migration.sql b/backend/prisma/migrations/20251018152641_add_permissions_to_forms/migration.sql new file mode 100644 index 0000000..e803d8b --- /dev/null +++ b/backend/prisma/migrations/20251018152641_add_permissions_to_forms/migration.sql @@ -0,0 +1,68 @@ +/* + Warnings: + + - The primary key for the `FieldAnswer` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `id` column on the `FieldAnswer` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - You are about to drop the column `isTemplate` on the `Form` table. All the data in the column will be lost. + - You are about to drop the column `slug` on the `Form` table. All the data in the column will be lost. + - The primary key for the `FormContributor` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `id` column on the `FormContributor` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - The primary key for the `FormField` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `id` column on the `FormField` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - The primary key for the `FormResponse` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The `id` column on the `FormResponse` table would be dropped and recreated. This will lead to data loss if there is data in the column. + - Changed the type of `responseId` on the `FieldAnswer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + - Changed the type of `fieldId` on the `FieldAnswer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- CreateEnum +CREATE TYPE "PermissionLevel" AS ENUM ('VIEW', 'EDIT'); + +-- DropForeignKey +ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_fieldId_fkey"; + +-- DropForeignKey +ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_responseId_fkey"; + +-- DropIndex +DROP INDEX "Form_slug_key"; + +-- AlterTable +ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_pkey", +DROP COLUMN "id", +ADD COLUMN "id" SERIAL NOT NULL, +DROP COLUMN "responseId", +ADD COLUMN "responseId" INTEGER NOT NULL, +DROP COLUMN "fieldId", +ADD COLUMN "fieldId" INTEGER NOT NULL, +ADD CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "Form" DROP COLUMN "isTemplate", +DROP COLUMN "slug", +ADD COLUMN "isEditable" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "FormContributor" DROP CONSTRAINT "FormContributor_pkey", +ADD COLUMN "permission" "PermissionLevel" NOT NULL DEFAULT 'VIEW', +DROP COLUMN "id", +ADD COLUMN "id" SERIAL NOT NULL, +ADD CONSTRAINT "FormContributor_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "FormField" DROP CONSTRAINT "FormField_pkey", +DROP COLUMN "id", +ADD COLUMN "id" SERIAL NOT NULL, +ADD CONSTRAINT "FormField_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "FormResponse" DROP CONSTRAINT "FormResponse_pkey", +DROP COLUMN "id", +ADD COLUMN "id" SERIAL NOT NULL, +ADD CONSTRAINT "FormResponse_pkey" PRIMARY KEY ("id"); + +-- AddForeignKey +ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e9b7d3c..7eac2e1 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -333,26 +333,31 @@ model Form { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt isActive Boolean @default(true) - slug String @unique - isTemplate Boolean @default(false) + isEditable Boolean @default(false) creator User @relation("UserForms", fields: [createdBy], references: [id]) contributors FormContributor[] fields FormField[] responses FormResponse[] } +enum PermissionLevel { + VIEW + EDIT +} + model FormContributor { - id String @id @default(cuid()) + id Int @id @default(autoincrement()) formId String userId String addedAt DateTime @default(now()) + permission PermissionLevel @default(VIEW) form Form @relation(fields: [formId], references: [id]) user User @relation(fields: [userId], references: [id]) @@unique([formId, userId]) } model FormField { - id String @id @default(cuid()) + id Int @id @default(autoincrement()) formId String label String fieldType FieldType @@ -364,7 +369,7 @@ model FormField { validation Json? conditions Json? form Form @relation(fields: [formId], references: [id]) - answers FieldAnswer[] + answers FieldAnswer[] } enum FieldType { @@ -382,9 +387,9 @@ enum FieldType { model FormResponse { - id String @id @default(cuid()) + id Int @id @default(autoincrement()) formId String - submittedAt DateTime @default(now()) + submittedAt DateTime @default(now()) submittedBy String? responderIp String? userAgent String? @@ -395,9 +400,9 @@ model FormResponse { } model FieldAnswer { - id String @id @default(cuid()) - responseId String - fieldId String + id Int @id @default(autoincrement()) + responseId Int + fieldId Int answerValue String? answerJson Json? response FormResponse @relation(fields: [responseId], references: [id]) diff --git a/backend/src/controllers/form.js b/backend/src/controllers/form.js new file mode 100644 index 0000000..3810972 --- /dev/null +++ b/backend/src/controllers/form.js @@ -0,0 +1,358 @@ +import prisma from '../config/database.js'; +import crypto from 'crypto'; + +const ensureAuth = (req) => { + if (!req.user || !req.user.id) throw { status: 401, message: 'Unauthorized' }; + return req.user; +}; + +function validateFieldRules(field) { + const { fieldType, validation } = field; + + if (!validation) return true; + + let rules = typeof validation === "string" ? JSON.parse(validation) : validation; + + switch (fieldType) { + case "TEXT": + if (rules.minLength && typeof rules.minLength !== "number") throw Error("minLength must be a number"); + if (rules.maxLength && typeof rules.maxLength !== "number") throw Error("maxLength must be a number"); + break; + case "NUMBER": + if (rules.min && typeof rules.min !== "number") throw Error("min must be a number"); + if (rules.max && typeof rules.max !== "number") throw Error("max must be a number"); + break; + case "EMAIL": + if (rules.pattern && typeof rules.pattern !== "string") throw Error("pattern must be a string regex"); + break; + case "FILE": + if (rules.allowedTypes && !Array.isArray(rules.allowedTypes)) throw Error("allowedTypes must be array"); + if (rules.maxSizeMB && typeof rules.maxSizeMB !== "number") throw Error("maxSizeMB must be number"); + break; + case "STAR_RATING": + if (rules.maxStars && typeof rules.maxStars !== "number") throw Error("maxStars must be number"); + break; + } + + return true; +} + +function validateFieldConditions(field, allFields) { + const { conditions } = field; + if (!conditions) return true; + + const cond = typeof conditions === "string" ? JSON.parse(conditions) : conditions; + + if (!cond.dependsOn || !cond.showIf) throw Error("Condition must have dependsOn and showIf"); + const dependentField = allFields.find(f => f.id === cond.dependsOn); + if (!dependentField) throw Error(`Condition dependsOn invalid field: ${cond.dependsOn}`); + + const validOps = ["equals", "notequals", "greaterthan", "lessthan","exists","notexists"]; + for (const op of Object.keys(cond.showIf)) { + if (!validOps.includes(op.toLowerCase())) throw Error(`Invalid operator in showIf: ${op}`); + } + + return true; +} + +export const createForm = async (req,res)=>{ + try { + const user = ensureAuth(req); + const { + title, + description, + isEditable=false, + fields=[], + contributors=[] + } = req.body; + + if(fields.length != 0){ + for (const field of fields) { + if(!field.fieldType || !field.label ||!field.position){ + return res.status(400).json({ error: "Each field must have 'fieldType' , 'label' and 'position'." }); + } + if (field.fieldType === "MULTIPLE_CHOICE" || field.fieldType === "DROPDOWN" || field.fieldType === "CHECKBOXES" || field.fieldType === "SINGLE_CHOICE") { + + let options = field.options; + + if (typeof options === "string") { + try { + options = JSON.parse(options); + } catch (err) { + return res.status(400).json({ error: "Invalid JSON in field options." }); + } + } + + if (!Array.isArray(options) || options.length < 2) { + return res.status(400).json({ error: "Multiple choice fields must have at least two options." }); + } + + for (const opt of options) { + if (!opt.label || !opt.value) { + return res.status(400).json({ error: "Each option must have 'label' and 'value'." }); + } + } + } + if(!validateFieldRules(field)){ + return res.status(400).json({ error: "Invalid field validation rules." }); + } + if(!validateFieldConditions(field, fields)){ + return res.status(400).json({ error: "Invalid field conditions." }); + } + } + } + + if(contributors.length!=0){ + for(const contributor of contributors){ + if(!contributor.userId || !contributor.permission){ + return res.status(400).json({ error: "Each contributor must have 'userId' and 'permission'." }); + } + if(!["VIEW","EDIT"].includes(contributor.permission)){ + return res.status(400).json({ error: "Contributor permission must be either 'VIEW' or 'EDIT'." }); + } + } + } + const formUrl = crypto.randomBytes(6).toString('hex').toUpperCase(); + + await prisma.$transaction(async (tx) => { + const form = await tx.form.create({ + data: { title, description, createdBy: user.id, formUrl, isEditable }, + }); + + if (fields.length > 0) { + const fieldsData = fields.map(f => ({ + formId: form.id, + label: f.label, + fieldType: f.fieldType, + position: f.position, + placeholder: f.placeholder || null, + isRequired: f.isRequired || false, + helpText: f.helpText || null, + options: f.options ? (typeof f.options === "string" ? JSON.parse(f.options) : f.options) : null, + validation: f.validation ? (typeof f.validation === "string" ? JSON.parse(f.validation) : f.validation) : null, + conditions: f.conditions ? (typeof f.conditions === "string" ? JSON.parse(f.conditions) : f.conditions) : null, + })); + + await tx.formField.createMany({ data: fieldsData }); + } + + if (contributors.length > 0) { + const contributorsData = contributors.map(c => ({ + formId: form.id, + userId: c.userId, + permission: c.permission, + })); + + await tx.formContributor.createMany({ data: contributorsData }); + } + + return form; + }); + + res.status(201).json({ message: "Form created successfully" }); + + } catch (error) { + res.status(500).json({ error: 'Failed to create forms ' + error, }); + } +} + +export const updateForm = async (req, res) => { + try { + const user = ensureAuth(req); + const { id } = req.params; + const { title, description, fields = [], contributors = [], isActive, isEditable } = req.body; + + const existingForm = await prisma.form.findUnique({ + where: { id }, + include: { + contributors: true, + fields: { include: { answers: true } }, + }, + }); + + if (!existingForm) return res.status(404).json({ error: 'Form not found' }); + if (!existingForm.isActive) return res.status(400).json({ error: 'Cannot edit an inactive form' }); + + const isOwner = existingForm.createdBy === user.id; + const canEdit = existingForm.contributors.some(c => c.userId === user.id && c.permission === 'EDIT'); + if (!isOwner && !canEdit) return res.status(403).json({ error: 'Permission denied' }); + + for (const field of fields) { + if (!field.label || !field.fieldType || field.position === undefined) + return res.status(400).json({ error: "Each field must have 'label', 'fieldType' and 'position'" }); + + if (["MULTIPLE_CHOICE", "SINGLE_CHOICE", "DROPDOWN", "CHECKBOXES"].includes(field.fieldType)) { + let options = field.options; + if (typeof options === "string") options = JSON.parse(options); + if (!Array.isArray(options) || options.length < 2) + return res.status(400).json({ error: 'Choice fields must have at least 2 options' }); + for (const opt of options) if (!opt.label || !opt.value) + return res.status(400).json({ error: "Each option must have 'label' and 'value'" }); + } + + if (!validateFieldRules(field)) return res.status(400).json({ error: 'Invalid validation rules' }); + if (!validateFieldConditions(field, fields)) return res.status(400).json({ error: 'Invalid conditions' }); + } + + for (const contributor of contributors) { + if (!contributor.userId || !contributor.permission) + return res.status(400).json({ error: "Each contributor must have 'userId' and 'permission'" }); + if (!["VIEW", "EDIT"].includes(contributor.permission)) + return res.status(400).json({ error: "Contributor permission must be either 'VIEW' or 'EDIT'" }); + } + + await prisma.$transaction(async (tx) => { + const updateData = {}; + if (title !== undefined) updateData.title = title; + if (description !== undefined) updateData.description = description; + if (isActive !== undefined) updateData.isActive = isActive; + if (isEditable !== undefined) updateData.isEditable = isEditable; + if (Object.keys(updateData).length > 0) { + await tx.form.update({ where: { id }, data: updateData }); + } + + const existingFieldsMap = new Map(existingForm.fields.map(f => [f.label, f])); + + const fieldsToCreate = []; + const fieldsToUpdate = []; + const fieldIdsToDelete = []; + + for (const field of fields) { + const existingField = existingFieldsMap.get(field.label); + if (existingField) { + const updateData = {}; + if (existingField.position !== field.position) updateData.position = field.position; + if (existingField.placeholder !== field.placeholder) updateData.placeholder = field.placeholder || null; + if (existingField.isRequired !== field.isRequired) updateData.isRequired = field.isRequired || false; + if (existingField.helpText !== field.helpText) updateData.helpText = field.helpText || null; + if (JSON.stringify(existingField.options) !== JSON.stringify(field.options)) + updateData.options = typeof field.options === "string" ? JSON.parse(field.options) : field.options; + if (JSON.stringify(existingField.validation) !== JSON.stringify(field.validation)) + updateData.validation = typeof field.validation === "string" ? JSON.parse(field.validation) : field.validation; + if (JSON.stringify(existingField.conditions) !== JSON.stringify(field.conditions)) + updateData.conditions = typeof field.conditions === "string" ? JSON.parse(field.conditions) : field.conditions; + + if (Object.keys(updateData).length > 0) { + fieldsToUpdate.push({ id: existingField.id, data: updateData }); + } + + existingFieldsMap.delete(field.label); + } else { + fieldsToCreate.push({ + formId: id, + label: field.label, + fieldType: field.fieldType, + position: field.position, + placeholder: field.placeholder || null, + isRequired: field.isRequired || false, + helpText: field.helpText || null, + options: field.options ? (typeof field.options === "string" ? JSON.parse(field.options) : field.options) : null, + validation: field.validation ? (typeof field.validation === "string" ? JSON.parse(field.validation) : field.validation) : null, + conditions: field.conditions ? (typeof field.conditions === "string" ? JSON.parse(field.conditions) : field.conditions) : null, + }); + } + } + + for (const [_, fieldToDelete] of existingFieldsMap) { + fieldIdsToDelete.push(fieldToDelete.id); + } + + if (fieldsToCreate.length > 0) await tx.formField.createMany({ data: fieldsToCreate }); + + for (const f of fieldsToUpdate) { + await tx.formField.update({ where: { id: f.id }, data: f.data }); + } + + if (fieldIdsToDelete.length > 0) { + await tx.fieldAnswer.deleteMany({ where: { fieldId: { in: fieldIdsToDelete } } }); + await tx.formField.deleteMany({ where: { id: { in: fieldIdsToDelete } } }); + } + + + const existingContributorsMap = new Map(existingForm.contributors.map(c => [c.userId, c])); + const contributorsToCreate = []; + const contributorsToUpdate = []; + const contributorIdsToDelete = []; + + for (const contributor of contributors) { + const existingContributor = existingContributorsMap.get(contributor.userId); + if (existingContributor) { + if (existingContributor.permission !== contributor.permission) + contributorsToUpdate.push({ id: existingContributor.id, permission: contributor.permission }); + existingContributorsMap.delete(contributor.userId); + } else { + contributorsToCreate.push({ formId: id, userId: contributor.userId, permission: contributor.permission }); + } + } + + for (const [_, cToDelete] of existingContributorsMap) contributorIdsToDelete.push(cToDelete.id); + + if (contributorsToCreate.length > 0) await tx.formContributor.createMany({ data: contributorsToCreate }); + + for (const c of contributorsToUpdate) + await tx.formContributor.update({ where: { id: c.id }, data: { permission: c.permission } }); + + if (contributorIdsToDelete.length > 0) + await tx.formContributor.deleteMany({ where: { id: { in: contributorIdsToDelete } } }); + + }); + + res.status(200).json({ message: 'Form updated successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to update form: ' + error }); + } +}; + + +export const deleteForm = async (req, res) => { + try { + const user = ensureAuth(req); + const { id } = req.params; + + const form = await prisma.form.findUnique({ + where: { id }, + include: { + fields: { include: { answers: true } }, + responses: { include: { answers: true } }, + }, + }); + + if (!form) return res.status(404).json({ error: 'Form not found' }); + if (form.createdBy !== user.id) { + return res.status(403).json({ error: 'Permission denied. Only the creator can delete this form.' }); + } + + await prisma.$transaction(async (tx) => { + const fieldIds = form.fields.map(f => f.id); + const responseIds = form.responses.map(r => r.id); + + if (fieldIds.length > 0) { + await tx.fieldAnswer.deleteMany({ where: { fieldId: { in: fieldIds } } }); + } + + if (responseIds.length > 0) { + await tx.fieldAnswer.deleteMany({ where: { responseId: { in: responseIds } } }); + } + + if (responseIds.length > 0) { + await tx.formResponse.deleteMany({ where: { id: { in: responseIds } } }); + } + + if (fieldIds.length > 0) { + await tx.formField.deleteMany({ where: { id: { in: fieldIds } } }); + } + + await tx.formContributor.deleteMany({ where: { formId: id } }); + + await tx.form.delete({ where: { id } }); + }); + + res.status(200).json({ message: 'Form and all associated data deleted successfully' }); + } catch (error) { + console.error('Delete form error:', error); + res.status(500).json({ error: 'Failed to delete form: ' + error.message }); + } +}; + + + From 4620427e068c93847f4feacc9cdfcbbc60d34646 Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sat, 18 Oct 2025 21:26:51 +0530 Subject: [PATCH 03/11] getters for forms --- backend/src/controllers/form.js | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/backend/src/controllers/form.js b/backend/src/controllers/form.js index 3810972..40ca817 100644 --- a/backend/src/controllers/form.js +++ b/backend/src/controllers/form.js @@ -354,5 +354,113 @@ export const deleteForm = async (req, res) => { } }; +export const getAllForms = async (req, res) => { + try { + const user = ensureAuth(req); + const forms = await prisma.form.findMany({ + where: { + OR: [ + { createdBy: user.id }, + { contributors: { some: { userId: user.id } } } + ] + } + }); + res.status(200).json(forms); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch forms: ' + error }); + } +} + +export const getFormByUrl = async (req, res) => { + try { + const { formUrl } = req.params; + const user = ensureAuth(req); + + const form = await prisma.form.findUnique({ + where: { formUrl }, + include: { + fields: { + include: { + answers: { + include: { + response: { + select: { + id: true, + submittedAt: true, + user: { select: { id: true, name: true, email: true } }, + }, + }, + }, + }, + }, + }, + contributors: { + select: { + userId: true, + permission: true, + user: { select: { id: true, name: true, email: true } }, + }, + }, + creator: { select: { id: true, name: true, email: true } }, + }, + }); + + + if (!form) return res.status(404).json({ error: 'Form not found' }); + const isOwner = form.createdBy === user.id; + const isContributor = form.contributors.some(c => c.userId === user.id); + if (!isOwner && !isContributor) { + return res.status(403).json({ error: 'Permission denied' }); + } + + const responseWise = form.responses.map(response => ({ + responseId: response.id, + submittedAt: response.submittedAt, + responderIp: response.responderIp, + userAgent: response.userAgent, + submittedBy: response.user ? { id: response.user.id, name: response.user.name, email: response.user.email } : null, + answers: response.answers.map(ans => ({ + fieldId: ans.fieldId, + fieldLabel: ans.field.label, + fieldType: ans.field.fieldType, + answerValue: ans.answerValue, + answerJson: ans.answerJson + })) + })); + + const questionWise = form.fields.map(field => ({ + id: field.id, + label: field.label, + fieldType: field.fieldType, + answers: field.answers.map(ans => ({ + responseId: ans.response.id, + submittedAt: ans.response.submittedAt, + submittedBy: ans.response.user + ? { id: ans.response.user.id, name: ans.response.user.name, email: ans.response.user.email } + : null, + answerValue: ans.answerValue, + answerJson: ans.answerJson, + })), + })); + + res.status(200).json({ + id: form.id, + title: form.title, + formUrl: form.formUrl, + description: form.description, + isActive: form.isActive, + isTemplate: form.isTemplate, + isEditable: form.isEditable, + fields: form.fields, + contributors: form.contributors, + creator: form.creator, + responsesCount: form.responses.length, + responseWise, + questionWise + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch form: ' + error }); + } +}; From 43dff906c4329f910151b48b042540779f04e7a2 Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sun, 19 Oct 2025 07:43:19 +0530 Subject: [PATCH 04/11] add controllers for form response (user side) and remove ip / user browser details from db schema --- .../migration.sql | 16 + backend/prisma/schema.prisma | 7 +- backend/src/controllers/form.js | 368 +++++++++++++++++- 3 files changed, 376 insertions(+), 15 deletions(-) create mode 100644 backend/prisma/migrations/20251019021225_form_response_model_changes/migration.sql diff --git a/backend/prisma/migrations/20251019021225_form_response_model_changes/migration.sql b/backend/prisma/migrations/20251019021225_form_response_model_changes/migration.sql new file mode 100644 index 0000000..f6c8b3d --- /dev/null +++ b/backend/prisma/migrations/20251019021225_form_response_model_changes/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `metadata` on the `FormResponse` table. All the data in the column will be lost. + - You are about to drop the column `responderIp` on the `FormResponse` table. All the data in the column will be lost. + - You are about to drop the column `userAgent` on the `FormResponse` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "FormField" ADD COLUMN "allowMultiple" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "FormResponse" DROP COLUMN "metadata", +DROP COLUMN "responderIp", +DROP COLUMN "userAgent", +ADD COLUMN "anonymousId" TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 7eac2e1..0341cee 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -362,6 +362,7 @@ model FormField { label String fieldType FieldType isRequired Boolean @default(false) + allowMultiple Boolean @default(false) options Json? position Int placeholder String? @@ -390,10 +391,8 @@ model FormResponse { id Int @id @default(autoincrement()) formId String submittedAt DateTime @default(now()) - submittedBy String? - responderIp String? - userAgent String? - metadata Json? + submittedBy String? + anonymousId String? form Form @relation(fields: [formId], references: [id]) user User? @relation("UserFormResponses", fields: [submittedBy], references: [id]) answers FieldAnswer[] diff --git a/backend/src/controllers/form.js b/backend/src/controllers/form.js index 40ca817..6ba2417 100644 --- a/backend/src/controllers/form.js +++ b/backend/src/controllers/form.js @@ -121,16 +121,17 @@ export const createForm = async (req,res)=>{ if (fields.length > 0) { const fieldsData = fields.map(f => ({ - formId: form.id, - label: f.label, - fieldType: f.fieldType, - position: f.position, - placeholder: f.placeholder || null, - isRequired: f.isRequired || false, - helpText: f.helpText || null, - options: f.options ? (typeof f.options === "string" ? JSON.parse(f.options) : f.options) : null, - validation: f.validation ? (typeof f.validation === "string" ? JSON.parse(f.validation) : f.validation) : null, - conditions: f.conditions ? (typeof f.conditions === "string" ? JSON.parse(f.conditions) : f.conditions) : null, + formId: form.id, + label: f.label, + fieldType: f.fieldType, + position: f.position, + placeholder: f.placeholder || null, + isRequired: f.isRequired || false, + allowMultiple: f.allowMultiple || false, + helpText: f.helpText || null, + options: f.options ? (typeof f.options === "string" ? JSON.parse(f.options) : f.options) : null, + validation: f.validation ? (typeof f.validation === "string" ? JSON.parse(f.validation) : f.validation) : null, + conditions: f.conditions ? (typeof f.conditions === "string" ? JSON.parse(f.conditions) : f.conditions) : null, })); await tx.formField.createMany({ data: fieldsData }); @@ -224,6 +225,7 @@ export const updateForm = async (req, res) => { if (existingField.position !== field.position) updateData.position = field.position; if (existingField.placeholder !== field.placeholder) updateData.placeholder = field.placeholder || null; if (existingField.isRequired !== field.isRequired) updateData.isRequired = field.isRequired || false; + if (existingField.allowMultiple !== field.allowMultiple) updateData.allowMultiple = field.allowMultiple || false; if (existingField.helpText !== field.helpText) updateData.helpText = field.helpText || null; if (JSON.stringify(existingField.options) !== JSON.stringify(field.options)) updateData.options = typeof field.options === "string" ? JSON.parse(field.options) : field.options; @@ -245,6 +247,7 @@ export const updateForm = async (req, res) => { position: field.position, placeholder: field.placeholder || null, isRequired: field.isRequired || false, + allowMultiple: field.allowMultiple || false, helpText: field.helpText || null, options: field.options ? (typeof field.options === "string" ? JSON.parse(field.options) : field.options) : null, validation: field.validation ? (typeof field.validation === "string" ? JSON.parse(field.validation) : field.validation) : null, @@ -303,7 +306,6 @@ export const updateForm = async (req, res) => { } }; - export const deleteForm = async (req, res) => { try { const user = ensureAuth(req); @@ -464,3 +466,347 @@ export const getFormByUrl = async (req, res) => { res.status(500).json({ error: 'Failed to fetch form: ' + error }); } }; + +export const getFormForDisplay = async (req, res) => { + try { + const { formUrl } = req.params; + + const form = await prisma.form.findUnique({ + where: { formUrl }, + include: { + fields: { + orderBy: { position: 'asc' }, + select: { + id: true, + label: true, + fieldType: true, + isRequired: true, + placeholder: true, + helpText: true, + options: true, + validation: true, + conditions: true, + } + }, + creator: { + select: { id: true, name: true, email: true } + } + } + }); + + if (!form || !form.isActive) { + return res.status(404).json({ error: "Form not found or inactive" }); + } + + const formattedFields = form.fields.map(field => ({ + id: field.id, + label: field.label, + type: field.fieldType, + isRequired: field.isRequired, + placeholder: field.placeholder || '', + helpText: field.helpText || '', + options: field.options ? (typeof field.options === 'string' ? JSON.parse(field.options) : field.options) : [], + validation: field.validation ? (typeof field.validation === 'string' ? JSON.parse(field.validation) : field.validation) : {}, + conditions: field.conditions ? (typeof field.conditions === 'string' ? JSON.parse(field.conditions) : field.conditions) : {} + })); + + res.status(200).json({ + form: { + id: form.id, + title: form.title, + description: form.description, + isEditable: form.isEditable, + createdAt: form.createdAt, + creator: form.creator, + fields: formattedFields + } + }); + + } catch (error) { + console.error('Error fetching form:', error); + res.status(500).json({ error: 'Failed to fetch form: ' + error.message }); + } +}; + +export const createFormResponse = async (req, res) => { + try { + const { formUrl } = req.params; + const { answers, submittedBy } = req.body; + + const form = await prisma.form.findUnique({ + where: { formUrl }, + include: { fields: true }, + }); + + if (!form) { + return res.status(404).json({ error: "Form not found" }); + } + + if (!Array.isArray(answers) || answers.length === 0) { + return res.status(400).json({ error: "Answers array is required" }); + } + const id = crypto.randomBytes(6).toString('hex').toUpperCase(); + + let anonymousId = null; + if (!submittedBy) { + if(form.isEditable) anonymousId = id; + } + + const response = await prisma.formResponse.create({ + data: { + formId: form.id, + submittedBy: submittedBy || null, + anonymousId, + userAgent, + }, + }); + + const fieldAnswersData = []; + + for (const ans of answers) { + const field = form.fields.find(f => f.id === ans.fieldId); + if (!field) { + return res.status(400).json({ error: `Invalid fieldId: ${ans.fieldId}` }); + } + + const allowedValues = Array.isArray(field.options) + ? field.options + : field.options?.values || []; + + if (["TEXT"].includes(field.fieldType)) { + if (!ans.answerValue || typeof ans.answerValue !== "string") { + return res.status(400).json({ error: `Text answer required for "${field.label}".` }); + } + } + else if (["SINGLE_CHOICE", "DROPDOWN"].includes(field.fieldType)) { + if (!allowedValues.includes(String(ans.answerValue))) { + return res.status(400).json({ error: `Invalid option for "${field.label}".` }); + } + } + else if (["MULTIPLE_CHOICE", "CHECKBOXES"].includes(field.fieldType)) { + const allowMultiple = field.allowMultiple ?? true; + const selections = Array.isArray(ans.answerJson) + ? ans.answerJson + : ans.answerValue + ? [ans.answerValue] + : []; + + if (selections.length === 0) { + return res.status(400).json({ error: `At least one option required for "${field.label}".` }); + } + + if (!allowMultiple && selections.length > 1) { + return res.status(400).json({ error: `"${field.label}" allows only one selection.` }); + } + + for (const val of selections) { + if (!allowedValues.includes(String(val))) { + return res.status(400).json({ error: `Invalid option "${val}" in "${field.label}".` }); + } + } + + fieldAnswersData.push({ + responseId: response.id, + fieldId: field.id, + answerJson: selections, + }); + continue; + } + + fieldAnswersData.push({ + responseId: response.id, + fieldId: field.id, + answerValue: ans.answerValue ?? null, + }); + } + + + await prisma.fieldAnswer.createMany({ + data: fieldAnswersData, + }); + + if(form.isEditable){ + res.status(201).json({ + responseId: response.id, + anonymousId, + }); + }else{ + res.status(201).json({ message: "Form response submitted successfully" }); + } + } catch (error) { + res.status(500).json({ error: "Failed to submit form response: " + error }); + } +}; + +export const updateFormResponse = async (req, res) => { + try { + const { formUrl } = req.params; + const { answers, submittedBy, anonymousId } = req.body; + + const form = await prisma.form.findUnique({ + where: { formUrl }, + include: { fields: true }, + }); + + if (!form) { + return res.status(404).json({ error: "Form not found" }); + } + + if (!form.isEditable) { + return res.status(200).json({ message: "This form cannot be edited." }); + } + + if(!submittedBy && !anonymousId){ + return res.status(403).json({ error: "Missing credentials to edit response." }); + } + + const response = await prisma.formResponse.findUnique({ + where: { + OR: [ + { submittedBy: submittedBy }, + { anonymousId: anonymousId } + ] + }, + include: { answers: true }, + }); + + if (!response) { + return res.status(404).json({ error: "Response not found" }); + } + + if (!Array.isArray(answers) || answers.length === 0) { + return res.status(400).json({ error: "Answers array is required." }); + } + + const fieldAnswersData = []; + + for (const ans of answers) { + const field = form.fields.find(f => f.id === ans.fieldId); + if (!field) { + return res.status(400).json({ error: `Invalid fieldId: ${ans.fieldId}` }); + } + + const allowedValues = Array.isArray(field.options) + ? field.options + : field.options?.values || []; + + if (["TEXT"].includes(field.fieldType)) { + if (!ans.answerValue || typeof ans.answerValue !== "string") { + return res.status(400).json({ error: `Text answer required for "${field.label}".` }); + } + } + else if (["SINGLE_CHOICE", "DROPDOWN"].includes(field.fieldType)) { + if (!allowedValues.includes(String(ans.answerValue))) { + return res.status(400).json({ error: `Invalid option for "${field.label}".` }); + } + } + else if (["MULTIPLE_CHOICE", "CHECKBOXES"].includes(field.fieldType)) { + const allowMultiple = field.allowMultiple ?? true; + const selections = Array.isArray(ans.answerJson) + ? ans.answerJson + : ans.answerValue + ? [ans.answerValue] + : []; + + if (selections.length === 0) { + return res.status(400).json({ error: `At least one option required for "${field.label}".` }); + } + + if (!allowMultiple && selections.length > 1) { + return res.status(400).json({ error: `"${field.label}" allows only one selection.` }); + } + + for (const val of selections) { + if (!allowedValues.includes(String(val))) { + return res.status(400).json({ error: `Invalid option "${val}" in "${field.label}".` }); + } + } + + fieldAnswersData.push({ + responseId: response.id, + fieldId: field.id, + answerJson: selections, + }); + continue; + } + + fieldAnswersData.push({ + responseId: response.id, + fieldId: field.id, + answerValue: ans.answerValue ?? null, + }); + } + + await prisma.$transaction(async tx => { + await tx.fieldAnswer.deleteMany({ + where: { responseId: response.id }, + }); + + await tx.fieldAnswer.createMany({ + data: fieldAnswersData, + }); + + await tx.formResponse.update({ + where: { id: response.id }, + data: { submittedAt: new Date() }, + }); + }); + + res.status(200).json({ + responseId: response.id, + }); + } catch (error) { + res.status(500).json({ error: "Failed to update form response: " + error }); + } +}; + +export const getFormResponses = async (req, res) => { + try { + const { formUrl } = req.params; + const { submittedBy, anonymousId } = req.body; + + const form = await prisma.form.findUnique({ + where: { formUrl }, + include: { + fields: true, + responses: { + include: { + answers: { + include: { field: { select: { id: true, label: true, fieldType: true } } } + }, + user: { select: { id: true, name: true, email: true } } + } + }, + creator: { select: { id: true } }, + } + }); + + if (!form) return res.status(404).json({ error: "Form not found" }); + if (!form.isEditable) { + return res.status(200).json({ message: "This form's responses are not editable." }); + } + const allowedResponses = form.responses.filter(r => r.anonymousId === anonymousId || r.submittedBy === submittedBy); + + if (!allowedResponses.length) { + return res.status(403).json({ error: "No editable responses found for this user" }); + } + + const responseWise = allowedResponses.map(response => ({ + responseId: response.id, + submittedAt: response.submittedAt, + submittedBy: response.user ? { id: response.user.id, name: response.user.name, email: response.user.email } : null, + answers: response.answers.map(ans => ({ + fieldId: ans.fieldId, + fieldLabel: ans.field.label, + fieldType: ans.field.fieldType, + answerValue: ans.answerValue ?? null, + answerJson: ans.answerJson ?? null + })) + })); + + res.status(200).json({ formId: form.id, title: form.title, responseWise }); + + } catch (error) { + res.status(500).json({ error: "Failed to fetch form responses: " + error }); + } +}; From 250a905d77b2b98a546dfdd1a320f92035a68e68 Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sun, 19 Oct 2025 07:54:51 +0530 Subject: [PATCH 05/11] add routes for forms in both backend and frontend --- backend/src/routes/form.js | 27 +++++++++++++++++++++ backend/src/routes/index.js | 2 ++ frontend/src/api/api.js | 47 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 backend/src/routes/form.js diff --git a/backend/src/routes/form.js b/backend/src/routes/form.js new file mode 100644 index 0000000..9874bbf --- /dev/null +++ b/backend/src/routes/form.js @@ -0,0 +1,27 @@ +import express from 'express'; +import { authenticateToken } from '../middleware/auth.js'; +import { + createForm, + updateForm, + deleteForm, + getAllForms, + getFormByUrl, + getFormForDisplay, + createFormResponse, + updateFormResponse, + getFormResponses +} from '../controllers/form.js'; + +const router = express.Router(); + +router.post('/', authenticateToken, createForm); +router.put('/:id', authenticateToken, updateForm); +router.delete('/:id', authenticateToken, deleteForm); +router.get('/my-forms', authenticateToken, getAllForms); +router.get('/manage/:formUrl', authenticateToken, getFormByUrl); +router.get('/display/:formUrl', getFormForDisplay); +router.post('/submit/:formUrl', createFormResponse); +router.put('/submit/:formUrl', updateFormResponse); +router.post('/responses/:formUrl', getFormResponses); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js index 2e950ce..dd4abff 100644 --- a/backend/src/routes/index.js +++ b/backend/src/routes/index.js @@ -1,11 +1,13 @@ import express from 'express'; import authRoutes from './auth.js'; import eventRoutes from './events.js'; +import formsRoutes from './forms.js'; import { checkJwt } from '../middleware/auth.js'; const router = express.Router(); router.use('/auth', authRoutes); router.use('/events', checkJwt, eventRoutes); // Protected route +router.use('/forms', formsRoutes); export default router; \ No newline at end of file diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index a5090cb..974c062 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -262,4 +262,51 @@ export const profileAPI = { api.put('/api/profile', profileData) }; +export const formsAPI = { + createForm: async (formData) => { + const response = await api.post('/forms', formData); + return response.data; + }, + + getUserForms: async () => { + const response = await api.get('/forms/my-forms'); + return response.data; + }, + + getFormForManage: async (formUrl) => { + const response = await api.get(`/forms/manage/${formUrl}`); + return response.data; + }, + + getFormForDisplay: async (formUrl) => { + const response = await api.get(`/forms/display/${formUrl}`); + return response.data; + }, + + updateForm: async (formId, formData) => { + const response = await api.put(`/forms/${formId}`, formData); + return response.data; + }, + + deleteForm: async (formId) => { + const response = await api.delete(`/forms/${formId}`); + return response.data; + }, + + submitFormResponse: async (formUrl, responseData) => { + const response = await api.post(`/forms/submit/${formUrl}`, responseData); + return response.data; + }, + + updateFormResponse: async (formUrl, responseData) => { + const response = await api.put(`/forms/submit/${formUrl}`, responseData); + return response.data; + }, + + getFormResponses: async (formUrl, credentials) => { + const response = await api.post(`/forms/responses/${formUrl}`, credentials); + return response.data; + } +}; + export default api; From ad82bff7af8b02a73ccf7292aa21a26aafbc043e Mon Sep 17 00:00:00 2001 From: Ashlesh Prabhu Date: Sun, 19 Oct 2025 21:47:34 +0530 Subject: [PATCH 06/11] first draft for ui of forms --- frontend/src/App.jsx | 34 ++ frontend/src/components/FormBuilder.jsx | 443 ++++++++++++++++++++ frontend/src/components/FormFieldEditor.jsx | 437 +++++++++++++++++++ frontend/src/components/FormPreview.jsx | 359 ++++++++++++++++ frontend/src/components/FormSubmission.jsx | 286 +++++++++++++ frontend/src/components/FormsList.jsx | 285 +++++++++++++ frontend/src/components/Navbar.jsx | 26 +- frontend/src/pages/CreateForm.jsx | 26 ++ frontend/src/pages/EditForm.jsx | 81 ++++ frontend/src/pages/FormsPage.jsx | 8 + 10 files changed, 1984 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/FormBuilder.jsx create mode 100644 frontend/src/components/FormFieldEditor.jsx create mode 100644 frontend/src/components/FormPreview.jsx create mode 100644 frontend/src/components/FormSubmission.jsx create mode 100644 frontend/src/components/FormsList.jsx create mode 100644 frontend/src/pages/CreateForm.jsx create mode 100644 frontend/src/pages/EditForm.jsx create mode 100644 frontend/src/pages/FormsPage.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3f6f0c4..e31787e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -17,6 +17,12 @@ import { NavigationProvider } from './contexts/NavigationContext'; import OrganiserDashboard from './pages/OrganiserDashboard.jsx'; import EventDashboard from './pages/EventDashboard.jsx'; +// Import form components +import FormsPage from './pages/FormsPage'; +import CreateForm from './pages/CreateForm'; +import EditForm from './pages/EditForm'; +import FormSubmission from './components/FormSubmission'; + const ProtectedRoute = ({ children, requiredRole }) => { const { isAuthenticated, isLoading, user } = useAuth(); @@ -98,6 +104,34 @@ const App = () => { } /> {/* NEW ROUTE */} } /> } /> + + {/* Forms Routes */} + + + + } + /> + + + + } + /> + + + + } + /> + {/* Public form submission route */} + } />