diff --git a/app/composables/useAdmin.ts b/app/composables/useAdmin.ts index 685b6bb..e5981ce 100644 --- a/app/composables/useAdmin.ts +++ b/app/composables/useAdmin.ts @@ -2,6 +2,7 @@ // Place this at: app/composables/useAdmin.ts import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' +import type { Form } from '~~/prisma/generated/client' dayjs.extend(utc) export const useAdmin = () => { @@ -81,19 +82,21 @@ export const useAdmin = () => { } const mapApiFormToUi = (form: any) => { - const questionList = Array.isArray(form.questions) ? form.questions : [] + const questionList = Array.isArray(form.Components) ? form.Components : [] + questionList.slice().sort((left: any, right: any) => left.order - right.order) + //replace direct type casting with better alternative, maybe zod coercing? return { id: Number(form.id), - weekStart: parseDateToYmd(form.weekStart || form.startDate || ''), - day: form.day || 'Monday', - title: form.title || `Form ${form.id}`, - date: form.date || formatDate(form.startDate || ''), - status: form.status || (form.published ? 'Active' : 'Unpublished'), - questions: questionList.map((question: any, index: number) => ({ - id: Number(question.id ?? index + 1), - type: question.type || question.questionType || 'text', - text: question.text || question.questionText || '', + weekStart: parseDateToYmd(form.startDate), //update to dayjs format function + day: dayjs(form.startDate).format('dddd'), + title: form.title, + date: formatDate(form.startDate || ''), //update to dayjs format function + status: form.published ? 'Active' : 'Unpublished', + questions: questionList.map((question: any) => ({ + id: question.id, + type: question.questionType || 'text', + text: question.questionText || '', textEs: question.questionOptions?.textEs || '', reference: question.questionOptions?.reference || '', referenceEs: question.questionOptions?.referenceEs || '', @@ -220,10 +223,8 @@ export const useAdmin = () => { const loadPublishedForms = async () => { try { - const forms = await callFormApi('GET', { - action: 'listForms', - weeklyDate: historyWeekStart.value || undefined, - }) + const forms = await $fetch('/api/form/list', + { query: { weeklyDate: historyWeekStart.value || undefined} }) publishedForms.value = (forms ?? []).map(mapApiFormToUi) } catch (error) { console.error('Failed to load forms', error) @@ -241,23 +242,20 @@ export const useAdmin = () => { const fallbackWeekEnd = dayjs.utc(fallbackWeekStart).add(6, 'day').format('YYYY-MM-DD') try { - const result = await callFormApi<{ - found: boolean - startDate: string | null - endDate: string | null - }>('GET', { - action: 'resolveFormGroupRangeByDate', - weeklyDate: historyWeekStart.value, + const result = await useFetch('/api/formGroup/matchByDate', { + method: 'GET', + query: {date: historyWeekStart.value} }) - if (!result?.found) { + if (!result.data.value) { historyGroupStartDate.value = fallbackWeekStart historyGroupEndDate.value = fallbackWeekEnd return } + result.data.value - historyGroupStartDate.value = parseDateToYmd(result.startDate || '') - historyGroupEndDate.value = parseDateToYmd(result.endDate || '') || fallbackWeekEnd + historyGroupStartDate.value = parseDateToYmd(result.data.value.startDate || '') + historyGroupEndDate.value = parseDateToYmd(result.data.value.endDate || '') || fallbackWeekEnd } catch (error) { console.error('Failed to resolve form group range', error) historyGroupStartDate.value = fallbackWeekStart @@ -324,12 +322,13 @@ export const useAdmin = () => { if (!startDate) throw new Error('Invalid week start date') startDate.setDate(startDate.getDate() + dayIndex) - await callFormApi('PUT', {}, { - action: 'updateForm', - id: editingFormId.value, - startDate: formatYmdLocal(startDate), - published: true, - title: formTitle.value, + await useFetch(`/api/form/${editingFormId.value}`, { + method: 'PUT', + body: { + startDate: formatYmdLocal(startDate), //swap to dayjs + published: true, + title: formTitle.value, + } }) const existingForm = publishedForms.value.find((form) => Number(form.id) === Number(editingFormId.value)) @@ -394,15 +393,16 @@ export const useAdmin = () => { } startDate.setDate(startDate.getDate() + dayIndex) - - const createdFormResponse = await callFormApi('POST', {}, { - action: 'createForm', - startDate: formatYmdLocal(startDate), - published: true, - title: formTitle.value, + const createdFormResponse: any = await useFetch('api/form', { + method: 'POST', + body: { + startDate: dayjs(startDate).toISOString(), + published: true, + title: formTitle.value, + } }) - const createdForm = createdFormResponse?.data + const createdForm: Form = createdFormResponse?.data.value if (!createdForm?.id) { continue diff --git a/app/composables/useCurrentFormGroup.ts b/app/composables/useCurrentFormGroup.ts index b3c2974..3761260 100644 --- a/app/composables/useCurrentFormGroup.ts +++ b/app/composables/useCurrentFormGroup.ts @@ -1,4 +1,5 @@ import type { FormGroup, Form, FormComponent } from '~~/prisma/generated/client' +import dayjs from 'dayjs' export type CurrentFormGroupState = { activeFormGroup: FormGroup | null @@ -13,40 +14,38 @@ export const useCurrentFormGroup = () => { formComponents: {} })) - const loadFormComponents = async (formId: number) => { - try { - const componentsAPIResponse = await $fetch('/api/formComponent', { - query: { form: formId } - }) - FormGroup.value.formComponents[formId] = Array.isArray(componentsAPIResponse) ? componentsAPIResponse : [] - } catch (error) { - console.error(`Failed to load form components for form ${formId}:`, error) - FormGroup.value.formComponents[formId] = [] - } - } - const loadActiveFormGroup = async () => { try { const formGroupAPIResponse = await $fetch('/api/formGroup?active=true') - // Handle if the API returns an array or single item const activeFg = Array.isArray(formGroupAPIResponse) ? formGroupAPIResponse[0] : formGroupAPIResponse if (activeFg) { FormGroup.value.activeFormGroup = activeFg try { - const formsAPIResponse = await $fetch('/api/form', { - query: { action: 'getOnlyActiveFormsinGroup', formGroup: activeFg.id } + const formsAPIResponse = await useFetch('/api/form/list', { + method: 'GET', + query: { published: 1, formGroup: activeFg.id } }) - - FormGroup.value.forms = Array.isArray(formsAPIResponse) ? formsAPIResponse : [] - - // Load form components for each form in parallel + FormGroup.value.formComponents = {} - await Promise.all( - FormGroup.value.forms.map(form => loadFormComponents(form.id)) - ) + FormGroup.value.forms = [] + + if (formsAPIResponse.data.value) { + FormGroup.value.formComponents = {} + FormGroup.value.forms = formsAPIResponse.data.value.map((form) => { + const {Components, FormGroup: _removedFormGroupField, startDate, endDate, ...restOfForm} = form + + FormGroup.value.formComponents[form.id] = Components ?? [] + + return { + ...restOfForm, + startDate: dayjs(startDate).toDate(), + endDate: endDate ? dayjs(endDate).toDate() : null, + } + }) + } } catch (error) { console.error('Failed to load forms for active form group:', error) FormGroup.value.forms = [] @@ -70,7 +69,6 @@ export const useCurrentFormGroup = () => { return { FormGroup, loadActiveFormGroup, - loadFormComponents, totalFormsInGroup } } \ No newline at end of file diff --git a/app/composables/useRaffleSpin.ts b/app/composables/useRaffleSpin.ts index 041c80f..6b0e635 100644 --- a/app/composables/useRaffleSpin.ts +++ b/app/composables/useRaffleSpin.ts @@ -9,13 +9,16 @@ export const useRaffleSpin = () => { const raffleWinner = ref(null) const spinCount = ref(0) + //match form group by date, raffle winner include added const loadRaffleFormGroup = async () => { const val = raffleWeekStart.value const dateStr = val instanceof Date ? val.toISOString().split('T')[0] : String(val).split('T')[0] + //merge formGroup/list.ts with index.ts const data = await $fetch(`/api/formGroup?date=${dateStr}`, { method: 'GET' }) raffleFormGroup.value = data || null } + //wtf is it even calling const loadRaffleForms = async () => { if (!raffleFormGroup.value) { raffleForms.value = [] @@ -59,6 +62,7 @@ export const useRaffleSpin = () => { const studentId = winningSubmission.student try { + //make formGroup/index.put.ts await $fetch(`/api/formGroup`, { method: 'PUT', body: { diff --git a/server/api/announcement/index.ts b/server/api/announcement/index.ts index 4918225..b8712aa 100644 --- a/server/api/announcement/index.ts +++ b/server/api/announcement/index.ts @@ -36,8 +36,8 @@ export default defineEventHandler(async (event) => { return await prisma.announcement.create({ data: { content: body.data.content, - postDate: new Date(body.data.postDate), - expiryDate: body.data.expiryDate ? new Date(body.data.expiryDate) : null, + postDate: body.data.postDate, + expiryDate: body.data.expiryDate ?? body.data.expiryDate, author: body.data.author, } }) diff --git a/server/api/form/[id].put.ts b/server/api/form/[id].put.ts new file mode 100644 index 0000000..bf6ed17 --- /dev/null +++ b/server/api/form/[id].put.ts @@ -0,0 +1,31 @@ +import { Prisma } from '~~/prisma/generated/client' +import { auth } from '~~/server/utils/auth' +import { prisma } from '~~/server/utils/prisma' +import { getQuery, setResponseStatus, type H3Event } from 'h3' +import { formUpdateSchema } from '~~/server/utils/schemas' +import { z } from 'zod' + +export default eventHandler(async (event) => { + + //require sessions + const idParam = getRouterParam(event, 'id') + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing ID" + }) + } + + const body = formUpdateSchema.safeParse(await readBody(event)) + + if (!body.success) {throw createError({ statusCode: 400, message: body.error.message })} + + const id = z.coerce.number().safeParse(idParam) + + if (!id.success) {throw createError({ statusCode: 400, message: 'Invalid form ID'})} + + return await prisma.form.update({ + where: { id: id.data }, + data: body.data + }) + }) \ No newline at end of file diff --git a/server/api/form/[id].ts b/server/api/form/[id].ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/api/form/index.post.ts b/server/api/form/index.post.ts new file mode 100644 index 0000000..ec2d656 --- /dev/null +++ b/server/api/form/index.post.ts @@ -0,0 +1,57 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import { formInclude } from '../../utils/prismaInclusions' +import { formCreateSchema } from '../../utils/schemas' + +export default defineEventHandler (async (event) => { + + //require admin + const body = await readBody(event) + + if (!body) { + throw createError({ + statusCode: 400, + message: "Missing Request Body" + }) + } + + const form = formCreateSchema.safeParse(body) + + if (!form.success) { + throw createError({ + statusCode: 400, + message: form.error.message + }) + } + + const requestedGroupExists = await prisma.formGroup.findUnique({ + where: {id: form.data.formGroup}, }).then(Boolean) + + if (!requestedGroupExists) { + throw createError({ + statusCode: 404, + message: "FormGroup Not Found" + }) + } + + const existingMax = await prisma.form.aggregate({ + where: { formGroup: form.data.formGroup }, + _max: { order: true }, + }) + + form.data.order = ((existingMax._max.order ?? -1) + 1) + + const created = await prisma.form.create({ + data: form.data as Prisma.FormUncheckedCreateInput, + include: formInclude + }) + + return { + success: true, + message: 'Form Created', + data: created + } + +}) \ No newline at end of file diff --git a/server/api/form/index.ts b/server/api/form/index.ts index f78e1e0..6b55fe8 100644 --- a/server/api/form/index.ts +++ b/server/api/form/index.ts @@ -2,23 +2,7 @@ import { Prisma } from '~~/prisma/generated/client' import { auth } from '~~/server/utils/auth' import { prisma } from '~~/server/utils/prisma' import { getQuery, setResponseStatus, type H3Event } from 'h3' - -type ActionName = - | 'listFormGroups' - | 'getOnlyActiveFormsinGroup' - | 'getFormGroup' - | 'resolveFormGroupRangeByDate' - | 'getFormGroupSubmissions' - | 'listForms' - | 'createFormGroup' - | 'createForm' - | 'createComponent' - | 'updateFormGroup' - | 'updateForm' - | 'updateComponent' - | 'deleteFormGroup' - | 'deleteForm' - | 'deleteComponent' +import {z } from 'zod' const WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] @@ -27,67 +11,6 @@ const hasOwnField = (value: Record | null, key: string) => const normalizeScalar = (value: unknown) => (Array.isArray(value) ? value[0] : value) -const toInt = (value: unknown, fieldName: string, required = true): number | null => { - const normalized = normalizeScalar(value) - - if (normalized === undefined || normalized === null || normalized === '') { - if (required) { - throw createError({ statusCode: 400, statusMessage: `${fieldName} is required` }) - } - - return null - } - - const parsed = typeof normalized === 'number' ? normalized : Number(normalized) - - if (!Number.isInteger(parsed)) { - throw createError({ statusCode: 400, statusMessage: `${fieldName} must be an integer` }) - } - - return parsed -} - -const toDate = (value: unknown, fieldName: string, required = true): Date | null => { - const normalized = normalizeScalar(value) - - if (normalized === undefined || normalized === null || normalized === '') { - if (required) { - throw createError({ statusCode: 400, statusMessage: `${fieldName} is required` }) - } - - return null - } - - if (typeof normalized === 'string') { - const dateOnlyMatch = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/) - - if (dateOnlyMatch) { - const [, year, month, day] = dateOnlyMatch - const parsed = new Date(Number(year), Number(month) - 1, Number(day)) - - if (Number.isNaN(parsed.getTime())) { - throw createError({ statusCode: 400, statusMessage: `${fieldName} must be a valid date` }) - } - - return parsed - } - } - - const parsed = new Date(String(normalized)) - - if (Number.isNaN(parsed.getTime())) { - throw createError({ statusCode: 400, statusMessage: `${fieldName} must be a valid date` }) - } - - return parsed -} - -const toBoolean = (value: unknown) => { - const normalized = normalizeScalar(value) - - return normalized === true || normalized === 'true' || normalized === 1 || normalized === '1' -} - const toOptionalJson = (value: unknown) => { if (value === undefined) { return undefined @@ -177,20 +100,6 @@ const findOrCreateWeeklyFormGroup = async (date: Date) => { return createdGroup.id } -const findMatchingFormGroupByDate = async (date: Date) => { - return await prisma.formGroup.findFirst({ - where: { - startDate: { lte: date }, - OR: [ - { endDate: null }, - { endDate: { gte: date } }, - ], - }, - orderBy: [{ startDate: 'desc' }, { id: 'desc' }], - select: { id: true, startDate: true, endDate: true }, - }) -} - const getAction = (event: H3Event, body: Record | null) => { const query = getQuery(event) return (query.action ?? body?.action) as ActionName | undefined @@ -199,46 +108,6 @@ const getAction = (event: H3Event, body: Record | null) => { const isFormApiDevBypassEnabled = () => process.env.NODE_ENV !== 'production' || process.env.FORM_API_DEV_BYPASS === 'true' -const requireAdminSession = async (event: H3Event) => { - const session = await auth.api.getSession({ - headers: event.headers, - }) - - if (!session) { - if (isFormApiDevBypassEnabled()) { - return { session: null, userId: null, admin: null, bypassed: true } - } - - throw createError({ - statusCode: 401, - statusMessage: 'Unauthorized: no active session. Log in, or set FORM_API_DEV_BYPASS=true for local testing only.', - }) - } - - const userId = normalizeScalar(session.user.id) - - if (!userId || typeof userId !== 'string') { - throw createError({ statusCode: 400, statusMessage: 'Invalid session user id' }) - } - - const admin = await prisma.admin.findUnique({ - where: { userId }, - }) - - if (!admin) { - if (isFormApiDevBypassEnabled()) { - return { session, userId, admin: null, bypassed: true } - } - - throw createError({ - statusCode: 403, - statusMessage: 'Forbidden: current user is not an admin.', - }) - } - - return { session, userId, admin, bypassed: false } -} - const mapComponent = (component: { id: number form: number @@ -302,555 +171,4 @@ const mapForm = ( } } -const formGroupInclude = { - RaffleWinner: true, - Forms: { - orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], - include: { - Components: { - orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], - }, - }, - }, -} - -const formInclude = { - Components: { - orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], - }, - FormGroup: true, -} - -export default defineEventHandler(async (event) => { - const method = event.node.req.method ?? 'GET' - const body = method === 'GET' ? null : ((await readBody(event).catch(() => null)) as Record | null) - const action = getAction(event, body) - - if (method !== 'GET' && !action) { throw createError({ statusCode: 400, statusMessage: 'Missing action' }) } - - if (method === 'GET') { - const selectedAction = action ?? 'listFormGroups' - - if (selectedAction === 'listFormGroups') { - const groups = await prisma.formGroup.findMany({ - orderBy: [{ startDate: 'desc' }, { id: 'desc' }], - include: formGroupInclude, - }) - - return groups.map((group) => ({ - id: group.id, - startDate: formatIsoDate(group.startDate), - endDate: formatIsoDate(group.endDate), - raffleWinner: group.raffleWinner, - raffleWinnerStudent: group.RaffleWinner, - forms: group.Forms.map((form) => mapForm(form, group.startDate)), - })) - } - - if (selectedAction === 'getFormGroup') { - const groupId = toInt(getQuery(event).id, 'id') - - const group = await prisma.formGroup.findUnique({ - where: { id: groupId as number }, - include: formGroupInclude, - }) - - if (!group) { - throw createError({ statusCode: 404, statusMessage: 'Form group not found' }) - } - - return { - id: group.id, - startDate: formatIsoDate(group.startDate), - endDate: formatIsoDate(group.endDate), - raffleWinner: group.raffleWinner, - raffleWinnerStudent: group.RaffleWinner, - forms: group.Forms.map((form) => mapForm(form, group.startDate)), - } - } - - if (selectedAction === 'listForms') { - const query = getQuery(event) - const formGroupId = query.formGroup !== undefined ? toInt(query.formGroup, 'formGroup', false) : null - const weeklyDate = - !!query.weeklyDate - ? (toDate(query.weeklyDate, 'weeklyDate') as Date) - : null - - const where: Prisma.FormWhereInput = {} - - if (formGroupId !== null) { where.formGroup = formGroupId } - - if (weeklyDate) { - const matchingGroup = await findMatchingFormGroupByDate(weeklyDate) - - if (!matchingGroup) { - return [] - } - - where.formGroup = matchingGroup.id - where.startDate = matchingGroup.endDate - ? { gte: matchingGroup.startDate, lte: matchingGroup.endDate } - : { gte: matchingGroup.startDate } - } - - if (query.published) { where.published = toBoolean(query.published) } - - const forms = await prisma.form.findMany({ - where, - orderBy: [{ formGroup: 'asc' }, { order: 'asc' }, { id: 'asc' }], - include: formInclude, - }) - - return forms.map((form) => mapForm(form, form.FormGroup.startDate)) - } - - if (selectedAction === 'resolveFormGroupRangeByDate') { - const weeklyDate = toDate(getQuery(event).weeklyDate, 'weeklyDate') as Date - const matchingGroup = await findMatchingFormGroupByDate(weeklyDate) - - if (!matchingGroup) { - return { - found: false, - formGroupId: null, - startDate: null, - endDate: null, - } - } - - return { - found: true, - formGroupId: matchingGroup.id, - startDate: formatIsoDate(matchingGroup.startDate), - endDate: formatIsoDate(matchingGroup.endDate), - } - } - - if (selectedAction === 'getFormGroupSubmissions') { - const formGroupId = toInt(getQuery(event).formGroupId, 'formGroupId') - - const forms = await prisma.form.findMany({ - where: { formGroup: formGroupId as number }, - select: { id: true }, - }) - - const formIds = forms.map((f) => f.id) - - if (formIds.length === 0) { - return { submissions: [], totalEntries: 0 } - } - - const submissions = await prisma.formSubmission.findMany({ - where: { form: { in: formIds } }, - include: { - Student: true, - Form: { select: { id: true, title: true, startDate: true } }, - }, - }) - - return { - submissions: submissions.map((sub) => ({ - id: sub.id, - studentId: sub.student, - studentName: sub.Student.name, - formId: sub.form, - formTitle: sub.Form.title ?? `Form ${sub.Form.id}`, - submissionDate: sub.submissionDate.toISOString(), - })), - totalEntries: submissions.length, - } - } - - if (selectedAction === 'getOnlyActiveFormsinGroup') { - const query = getQuery(event) - const where: Prisma.FormWhereInput = {} - - if (query.formGroup !== null) { where.formGroup = Number(query.formGroup) } - - if (query.published) { where.published = toBoolean(query.published) } - - return await prisma.form.findMany({ - where, - orderBy: [{ formGroup: 'asc' }, { order: 'asc' }, { id: 'asc' }], - }) - } - - - throw createError({ statusCode: 400, statusMessage: 'Unknown action' }) - } - - - - const { admin } = await requireAdminSession(event) - - if (method === 'POST') { - if (action === 'createFormGroup') { - const startDate = toDate(body?.startDate, 'startDate') - const endDate = hasOwnField(body, 'endDate') ? toDate(body?.endDate, 'endDate', false) : null - const raffleWinner = hasOwnField(body, 'raffleWinner') ? toInt(body?.raffleWinner, 'raffleWinner', false) : null - - if (raffleWinner !== null) { - const student = await prisma.student.findUnique({ - where: { id: raffleWinner }, - select: { id: true }, - }) - - if (!student) { - throw createError({ statusCode: 404, statusMessage: 'Raffle winner student not found' }) - } - } - - const created = await prisma.formGroup.create({ - data: { - startDate: startDate as Date, - endDate, - raffleWinner, - }, - include: formGroupInclude, - }) - - setResponseStatus(event, 201) - - return { - success: true, - message: 'Form group created', - data: { - id: created.id, - startDate: formatIsoDate(created.startDate), - endDate: formatIsoDate(created.endDate), - raffleWinner: created.raffleWinner, - raffleWinnerStudent: created.RaffleWinner, - forms: created.Forms.map((form) => mapForm(form, created.startDate)), - }, - } - } - - if (action === 'createForm') { - const startDate = toDate(body?.startDate, 'startDate') as Date - const endDate = hasOwnField(body, 'endDate') ? toDate(body?.endDate, 'endDate', false) : null - const published = hasOwnField(body, 'published') ? toBoolean(body?.published) : false - const title = hasOwnField(body, 'title') ? String(body?.title ?? '').trim() || null : null - const explicitOrder = hasOwnField(body, 'order') ? toInt(body?.order, 'order', false) : null - const requestedGroupId = hasOwnField(body, 'formGroup') - ? toInt(body?.formGroup, 'formGroup', false) - : null - - let resolvedFormGroupId: number - - if (requestedGroupId !== null) { - const requestedGroup = await prisma.formGroup.findUnique({ - where: { id: requestedGroupId }, - select: { id: true }, - }) - - resolvedFormGroupId = requestedGroup - ? requestedGroup.id - : await findOrCreateWeeklyFormGroup(startDate) - } else { - resolvedFormGroupId = await findOrCreateWeeklyFormGroup(startDate) - } - - const existingMax = await prisma.form.aggregate({ - where: { formGroup: resolvedFormGroupId }, - _max: { order: true }, - }) - - const createData: Record = { - formGroup: resolvedFormGroupId, - startDate, - endDate, - published, - order: explicitOrder ?? ((existingMax._max.order ?? -1) + 1), - author: admin?.id ?? null, - } - - if (title) { - createData.title = title - } - - const created = await prisma.form.create({ - data: createData as Prisma.FormUncheckedCreateInput, - include: formInclude, - }) - - setResponseStatus(event, 201) - - return { - success: true, - message: 'Form created', - data: mapForm(created, created.FormGroup.startDate), - } - } - - if (action === 'createComponent') { - const form = toInt(body?.form, 'form') - const questionType = String(body?.questionType ?? '').trim() - const questionText = String(body?.questionText ?? '').trim() - - if (!questionType) { - throw createError({ statusCode: 400, statusMessage: 'questionType is required' }) - } - - if (!questionText) { - throw createError({ statusCode: 400, statusMessage: 'questionText is required' }) - } - - const parentForm = await prisma.form.findUnique({ - where: { id: form as number }, - select: { id: true }, - }) - - if (!parentForm) { - throw createError({ statusCode: 404, statusMessage: 'Form not found' }) - } - - const explicitOrder = hasOwnField(body, 'order') ? toInt(body?.order, 'order', false) : null - - const existingMax = await prisma.formComponent.aggregate({ - where: { form: form as number }, - _max: { order: true }, - }) - - const created = await prisma.formComponent.create({ - data: { - form: form as number, - order: explicitOrder ?? ((existingMax._max.order ?? -1) + 1), - questionType, - questionText, - questionOptions: toOptionalJson(body?.questionOptions) ?? null, - }, - }) - - setResponseStatus(event, 201) - - return { - success: true, - message: 'Form component created', - data: mapComponent(created), - } - } - - throw createError({ statusCode: 400, statusMessage: 'Unknown action' }) - } - - if (method === 'PUT') { - if (action === 'updateFormGroup') { - const id = toInt(body?.id, 'id') - const data: { - startDate?: Date - endDate?: Date | null - raffleWinner?: number | null - } = {} - - if (hasOwnField(body, 'startDate')) { - data.startDate = toDate(body?.startDate, 'startDate') as Date - } - - if (hasOwnField(body, 'endDate')) { - data.endDate = toDate(body?.endDate, 'endDate', false) - } - - if (hasOwnField(body, 'raffleWinner')) { - const raffleWinner = toInt(body?.raffleWinner, 'raffleWinner', false) - - if (raffleWinner !== null) { - const student = await prisma.student.findUnique({ - where: { id: raffleWinner }, - select: { id: true }, - }) - - if (!student) { - throw createError({ statusCode: 404, statusMessage: 'Raffle winner student not found' }) - } - } - - data.raffleWinner = raffleWinner - } - - const updated = await prisma.formGroup.update({ - where: { id: id as number }, - data, - include: formGroupInclude, - }) - - return { - success: true, - message: 'Form group updated', - data: { - id: updated.id, - startDate: formatIsoDate(updated.startDate), - endDate: formatIsoDate(updated.endDate), - raffleWinner: updated.raffleWinner, - raffleWinnerStudent: updated.RaffleWinner, - forms: updated.Forms.map((form) => mapForm(form, updated.startDate)), - }, - } - } - - if (action === 'updateForm') { - const id = toInt(body?.id, 'id') - const data: { - formGroup?: number - startDate?: Date - endDate?: Date | null - published?: boolean - order?: number - title?: string | null - } = {} - - if (hasOwnField(body, 'formGroup')) { - const formGroup = toInt(body?.formGroup, 'formGroup') - const group = await prisma.formGroup.findUnique({ - where: { id: formGroup as number }, - select: { id: true }, - }) - - if (!group) { - throw createError({ statusCode: 404, statusMessage: 'Form group not found' }) - } - - data.formGroup = formGroup as number - } - - if (hasOwnField(body, 'startDate')) { - data.startDate = toDate(body?.startDate, 'startDate') as Date - } - - if (hasOwnField(body, 'endDate')) { - data.endDate = toDate(body?.endDate, 'endDate', false) - } - - if (hasOwnField(body, 'published')) { - data.published = toBoolean(body?.published) - } - - if (hasOwnField(body, 'order')) { - data.order = toInt(body?.order, 'order') as number - } - - if (hasOwnField(body, 'title')) { - const title = String(body?.title ?? '').trim() - data.title = title || null - } - - const updated = await prisma.form.update({ - where: { id: id as number }, - data: data as Prisma.FormUncheckedUpdateInput, - include: formInclude, - }) - - return { - success: true, - message: 'Form updated', - data: mapForm(updated, updated.FormGroup.startDate), - } - } - - if (action === 'updateComponent') { - const id = toInt(body?.id, 'id') - const data: Prisma.FormComponentUpdateInput = {} - - if (hasOwnField(body, 'form')) { - const form = toInt(body?.form, 'form') - const parentForm = await prisma.form.findUnique({ - where: { id: form as number }, - select: { id: true }, - }) - - if (!parentForm) { - throw createError({ statusCode: 404, statusMessage: 'Form not found' }) - } - - data.Form = { - connect: { id: form as number }, -} - } - - if (hasOwnField(body, 'order')) { - data.order = toInt(body?.order, 'order') as number - } - - if (hasOwnField(body, 'questionType')) { - const questionType = String(body?.questionType ?? '').trim() - - if (!questionType) { - throw createError({ statusCode: 400, statusMessage: 'questionType is required' }) - } - - data.questionType = questionType - } - - if (hasOwnField(body, 'questionText')) { - const questionText = String(body?.questionText ?? '').trim() - - if (!questionText) { - throw createError({ statusCode: 400, statusMessage: 'questionText is required' }) - } - - data.questionText = questionText - } - - if (hasOwnField(body, 'questionOptions')) { - data.questionOptions = toOptionalJson(body?.questionOptions) - } - - const updated = await prisma.formComponent.update({ - where: { id: id as number }, - data, - }) - - return { - success: true, - message: 'Form component updated', - data: mapComponent(updated), - } - } - - throw createError({ statusCode: 400, statusMessage: 'Unknown action' }) - } - - if (method === 'DELETE') { - if (action === 'deleteFormGroup') { - const id = toInt(body?.id ?? getQuery(event).id, 'id') - - await prisma.formGroup.delete({ - where: { id: id as number }, - }) - - return { - success: true, - message: 'Form group deleted', - } - } - - if (action === 'deleteForm') { - const id = toInt(body?.id ?? getQuery(event).id, 'id') - - await prisma.form.delete({ - where: { id: id as number }, - }) - - return { - success: true, - message: 'Form deleted', - } - } - - if (action === 'deleteComponent') { - const id = toInt(body?.id ?? getQuery(event).id, 'id') - - await prisma.formComponent.delete({ - where: { id: id as number }, - }) - - return { - success: true, - message: 'Form component deleted', - } - } - - throw createError({ statusCode: 400, statusMessage: 'Unknown action' }) - } - - throw createError({ statusCode: 405, statusMessage: 'Method not allowed' }) -}) +export default defineEventHandler(async (event) => {}) diff --git a/server/api/form/list.get.ts b/server/api/form/list.get.ts new file mode 100644 index 0000000..774f94a --- /dev/null +++ b/server/api/form/list.get.ts @@ -0,0 +1,53 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import z from 'zod' +import { formInclude } from '../../utils/prismaInclusions' + +export default defineEventHandler(async (event) => { + const query = getQuery(event) + + //add requireSessions + const formGroupId = z.coerce.number().safeParse(query.formGroupId).data + const weeklyDate = z.coerce.date().safeParse(query.weeklyDate).data + + const where: Prisma.FormWhereInput = {} + + //expecting 0/1 for f/t + if (query.published) { + const pre = z.coerce.number().safeParse(query.published) + if (!pre.success) { throw createError({ statusCode: 400, message: 'Invalid published value' })} + const published = z.coerce.boolean().safeParse(pre.data).data + where.published = published + } + + if (formGroupId) {where.formGroup = formGroupId} + + //warning/error received on nested fetch 'Invalid lazy handler result. It should be a function:\n' + //remove nested handling, use where.FormGroup + if (weeklyDate) { + const matchingGroup = await $fetch('/api/formGroup/matchByDate', { + query: { date: weeklyDate.toISOString() } + }) + + if (!matchingGroup) { + return [] + } + + where.formGroup = matchingGroup.id + where.startDate = matchingGroup.endDate + ? { gte: matchingGroup.startDate, lte: matchingGroup.endDate } + : { gte: matchingGroup.startDate } + } + + const forms = await prisma.form.findMany({ + where, + orderBy: [{ formGroup: 'asc' }, { order: 'asc' }, { id: 'asc' }], + include: formInclude, + }) + + return forms +}) + + \ No newline at end of file diff --git a/server/api/formComponent/[id].delete.ts b/server/api/formComponent/[id].delete.ts new file mode 100644 index 0000000..70f4465 --- /dev/null +++ b/server/api/formComponent/[id].delete.ts @@ -0,0 +1,37 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import z from 'zod' + + +export default defineEventHandler(async (event) => { + //require admin + + const idParam = getRouterParam(event, 'id') + + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing ID Parameter" + }) + } + + const id = z.coerce.number().safeParse(idParam) + + if (!id.success) { + throw createError({ + statusCode:400, + message: id.error.message + }) + } + + await prisma.formComponent.delete({ + where: { id: id.data } + }) + + return { + success: true, + message: 'Form Component Deleted' + } +}) \ No newline at end of file diff --git a/server/api/formComponent/[id].put.ts b/server/api/formComponent/[id].put.ts new file mode 100644 index 0000000..88fa4b2 --- /dev/null +++ b/server/api/formComponent/[id].put.ts @@ -0,0 +1,47 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { z } from 'zod' +import { requireAdmin, requireSession } from '../../utils/require-session' +import { formComponentUpdateSchema } from '../../utils/schemas' + +export default defineEventHandler(async (event) => { + //require admin + const idParam = getRouterParam(event, 'id') + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing ID" + }) + } + + const body = await readBody(event) + const component = formComponentUpdateSchema.safeParse(body) + + if (!component.success) {throw createError({ statusCode: 400, message: component.error.message })} + + const id = z.coerce.number().safeParse(idParam) + + if (!id.success) {throw createError({ statusCode: 400, message: 'Invalid ID Parameter'})} + + const formExists = await prisma.form.findUnique({ + where: {id: component.data.form} + }).then(Boolean) + + if (!formExists) { + throw createError({ + statusCode: 404, + message: "Form Not Found" + }) + } + const updated = await prisma.formComponent.update({ + where: {id: id.data}, + data: component.data as Prisma.FormComponentUncheckedUpdateInput + }) + + return { + success: true, + message: 'Form Component Updated', + data: updated + } +}) \ No newline at end of file diff --git a/server/api/formComponent/index.post.ts b/server/api/formComponent/index.post.ts new file mode 100644 index 0000000..60b06b4 --- /dev/null +++ b/server/api/formComponent/index.post.ts @@ -0,0 +1,59 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import { formInclude } from '../../utils/prismaInclusions' +import { formComponentCreateSchema } from '../../utils/schemas' + + +export default defineEventHandler(async (event) => { + //require admin + + const body = await readBody(event) + + if (!body) { + throw createError({ + statusCode: 400, + message: "Missing Request Body" + }) + } + + const component = formComponentCreateSchema.safeParse(body) + + if (!component.success) { + throw createError({ + statusCode:400, + message: component.error.message + }) + } + + const formExists = await prisma.form.findUnique({ + where: { id: component.data.form} + }).then(Boolean) + + if (!formExists) { + throw createError({ + statusCode: 404, + message: "Form Not Found" + }) + } + + const existingMax = await prisma.formComponent.aggregate({ + where: { form: component.data.form }, + _max: { order: true }, + }) + + component.data.order = component.data.order ?? ((existingMax._max.order ?? -1) + 1) + + const created = await prisma.formComponent.create({ + data: component.data as Prisma.FormComponentUncheckedCreateInput + }) + + setResponseStatus(event, 201) + + return { + success: true, + message: 'Form component created', + data: created + } +}) \ No newline at end of file diff --git a/server/api/formGroup/MatchByDate.get.ts b/server/api/formGroup/MatchByDate.get.ts new file mode 100644 index 0000000..d1748bb --- /dev/null +++ b/server/api/formGroup/MatchByDate.get.ts @@ -0,0 +1,38 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import z from 'zod' + +export default defineEventHandler(async (event) => { + //session check needed + const query = getQuery(event) + + if (!query.date) { + throw createError({ + statusCode: 400, + message: 'Missing Date', + }) + } + + const date = z.coerce.date().safeParse(query.date) + + if (!date.success) { + throw createError({ + statusCode: 400, + message: date.error.message + }) + } + + return await prisma.formGroup.findFirst({ + where: { + startDate: { lte: date.data }, + OR: [ + { endDate: null }, + { endDate: { gte: date.data } }, + ], + }, + orderBy: [{ startDate: 'desc' }, { id: 'desc' }], + select: { id: true, startDate: true, endDate: true } + }) +}) \ No newline at end of file diff --git a/server/api/formGroup/[id].delete.ts b/server/api/formGroup/[id].delete.ts new file mode 100644 index 0000000..b82c0a6 --- /dev/null +++ b/server/api/formGroup/[id].delete.ts @@ -0,0 +1,39 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import z from 'zod' +import dayjs from 'dayjs' +import { formGroupInclude } from '../../utils/prismaInclusions' + +export default defineEventHandler(async (event) => { + //require admin + + const idParam = getRouterParam(event, 'id') + + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing FormGroup id" + }) + } + + const group = z.coerce.number().safeParse(idParam) + + if (!group.success) { + throw createError({ + statusCode: 400, + message: group.error.message + }) + } + + await prisma.formGroup.delete({ + where: {id: group.data} + }) + + return { + success: true, + message: 'Form Group deleted', + } + +}) \ No newline at end of file diff --git a/server/api/formGroup/[id].get.ts b/server/api/formGroup/[id].get.ts new file mode 100644 index 0000000..626b4af --- /dev/null +++ b/server/api/formGroup/[id].get.ts @@ -0,0 +1,45 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import z from 'zod' +import dayjs from 'dayjs' +import { formGroupInclude } from '../../utils/prismaInclusions' + +export default defineEventHandler(async (event) => { + //add auth + const idParam = getRouterParam(event, 'id') + + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing FormGroup id" + }) + } + const groupId = z.coerce.number().safeParse(idParam) + + if (!groupId.success) { + throw createError({ + statusCode: 400, + message: groupId.error.message + }) + } + + const group = await prisma.formGroup.findUnique({ + where: { id: groupId.data as number }, + include: formGroupInclude, + }) + + if (!group) { + throw createError({ statusCode: 404, statusMessage: 'Form group not found' }) + } + + return { + id: group.id, + startDate: dayjs(group.startDate).toISOString(), + endDate: dayjs(group.endDate).toISOString(), + raffleWinner: group.raffleWinner, + raffleWinnerStudent: group.RaffleWinner, + forms: group.Forms, + } +}) \ No newline at end of file diff --git a/server/api/formGroup/[id].put.ts b/server/api/formGroup/[id].put.ts new file mode 100644 index 0000000..236d27d --- /dev/null +++ b/server/api/formGroup/[id].put.ts @@ -0,0 +1,39 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { z } from 'zod' +import { requireAdmin, requireSession } from '../../utils/require-session' +import { formGroupInclude } from '../../utils/prismaInclusions' +import { formGroupUpdateSchema } from '../../utils/schemas' + +export default defineEventHandler(async (event) => { + //require admin + const idParam = getRouterParam(event, 'id') + if (!idParam) { + throw createError({ + statusCode: 400, + message: "Missing ID" + }) + } + + const body = await readBody(event) + const group = formGroupUpdateSchema.safeParse(body) + + if (!group.success) {throw createError({ statusCode: 400, message: group.error.message })} + + const id = z.coerce.number().safeParse(idParam) + + if (!id.success) {throw createError({ statusCode: 400, message: 'Invalid ID Parameter'})} + + const updated = await prisma.formGroup.update({ + where: {id: id.data}, + data: group.data as Prisma.FormGroupUncheckedUpdateInput, + include: formGroupInclude + }) + + return { + success: true, + message: 'FormGroup Updated', + data: updated + } +}) \ No newline at end of file diff --git a/server/api/formGroup/[id].ts b/server/api/formGroup/[id].ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/api/formGroup/index.post.ts b/server/api/formGroup/index.post.ts new file mode 100644 index 0000000..662012b --- /dev/null +++ b/server/api/formGroup/index.post.ts @@ -0,0 +1,45 @@ +import { prisma } from '../../utils/prisma' +import { Prisma } from '~~/prisma/generated/client' +import { getQuery, createError } from 'h3' +import { requireAdmin, requireSession } from '../../utils/require-session' +import { formGroupInclude } from '../../utils/prismaInclusions' +import { formGroupCreateSchema } from '../../utils/schemas' + +export default defineEventHandler(async (event) => { + //require admin + const body = await readBody(event) + if (!body) { + throw createError({ + statusCode: 400, + message: "Missing Request Body" + }) + } + + const formGroup = formGroupCreateSchema.safeParse(body) + + if (!formGroup.success) { + throw createError({ + statusCode: 400, + message: formGroup.error.message + }) + } + + const created = await prisma.formGroup.create({ + data: formGroup.data as Prisma.FormGroupUncheckedCreateInput, + include: formGroupInclude, + }) + + return { + success: true, + message: 'Form group create', + data: { + id: created.id, + startDate: created.startDate, + endDate: created.endDate, + raffleWinner: null, + raffleWinnerStudent: null, + forms: created.Forms + } + } + +}) \ No newline at end of file diff --git a/server/api/formGroup/list.get.ts b/server/api/formGroup/list.get.ts new file mode 100644 index 0000000..6f317fe --- /dev/null +++ b/server/api/formGroup/list.get.ts @@ -0,0 +1,24 @@ +import { Prisma } from '~~/prisma/generated/client' +import { auth } from '~~/server/utils/auth' +import { prisma } from '~~/server/utils/prisma' +import { getQuery, setResponseStatus, type H3Event } from 'h3' +import {z } from 'zod' +import { formGroupInclude } from '../../utils/prismaInclusions' + + +export default eventHandler(async (event: H3Event) => { + //add auth + const groups = await prisma.formGroup.findMany({ + orderBy: [{ startDate: 'desc' }, { id: 'desc' }], + include: formGroupInclude + }) + + return groups.map((group) => ({ + id: group.id, + startDate: group.startDate, + endDate: group.endDate, + raffleWinner: group.raffleWinner, + raffleWinnerStudent: group.RaffleWinner, + forms: group.Forms, + })) +}) \ No newline at end of file diff --git a/server/utils/prismaInclusions.ts b/server/utils/prismaInclusions.ts new file mode 100644 index 0000000..84608a8 --- /dev/null +++ b/server/utils/prismaInclusions.ts @@ -0,0 +1,19 @@ +//Commonly Used Inclusions for prisma queries +export const formInclude = { + Components: { + orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], + }, + FormGroup: true, +} + +export const formGroupInclude = { + RaffleWinner: true, + Forms: { + orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], + include: { + Components: { + orderBy: [{ order: 'asc' as const }, { id: 'asc' as const }], + }, + }, + }, +} diff --git a/server/utils/schemas.ts b/server/utils/schemas.ts index 1350b7b..ee13f9a 100644 --- a/server/utils/schemas.ts +++ b/server/utils/schemas.ts @@ -42,26 +42,33 @@ export const announcementCreateSchema = z.object({ export const formGroupCreateSchema = z.object({ startDate: z.coerce.date(), - endDate: z.coerce.date(), + endDate: z.coerce.date().nullish(), }) -export const formGroupGETSchema = formGroupCreateSchema.extend({ - id: z.int(), - raffleWinner: z.int().nullish() +export const formGroupUpdateSchema = z.object({ + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().nullish(), + raffleWinner: z.int().optional() }) export const formCreateSchema = z.object({ - order: z.int(), + order: z.int().nullish(), startDate: z.coerce.date(), - endDate: z.coerce.date(), + endDate: z.coerce.date().nullish(), published: z.boolean(), - author: z.cuid2(), + author: z.cuid2().nullish(), formGroup: z.int(), title: z.string().min(1).max(250), }) -export const formGETSchema = formCreateSchema.extend({ - id: z.int() +export const formUpdateSchema = z.object({ + order: z.int().optional(), + startDate: z.coerce.date().optional(), + endDate: z.coerce.date().nullish(), + published: z.boolean().optional(), + author: z.cuid2().nullish(), + formGroup: z.int().optional(), + title: z.string().min(1).max(250).optional(), }) export const formComponentCreateSchema = z.object({ @@ -72,7 +79,11 @@ export const formComponentCreateSchema = z.object({ questionOptions: z.object({ choices: z.array(z.object({text: z.string().min(1), correct: z.boolean()})).optional(), video: z.string().optional() - }) + }).nullish() +}) + +export const formComponentUpdateSchema = z.object({ + }) export const formSubmissionCreateSchema = z.object({ @@ -94,9 +105,10 @@ export type StudentCreate = z.infer export type StudentUpdate = z.infer export type AnnouncementCreate = z.infer export type FormCreate = z.infer -export type FormGET = z.infer -export type FormGroupGET = z.infer +export type FormUpdate = z.infer export type FormGroupCreate = z.infer +export type FormGroupUpdate = z.infer export type FormComponentCreate = z.infer +export type FormComponentUpdate = z.infer export type FormSubmissionCreate = z.infer export type SubmissionResponseCreate = z.infer \ No newline at end of file