diff --git a/pages/api/lessons/index.ts b/pages/api/lessons/index.ts index 1d6c909..33b2055 100644 --- a/pages/api/lessons/index.ts +++ b/pages/api/lessons/index.ts @@ -1,9 +1,7 @@ import { NextApiHandler } from 'next'; -import { Lesson } from '@/domain/lesson'; -import { ApiResponse, extractSearchFilter, extractSearchOptions, HandlerCollection } from '@/utils/api'; +import { HandlerCollection } from '@/utils/api'; import { HttpCode } from '@/utils/http'; -import { PrismaClient } from '@prisma/client'; -import prisma from 'db/prisma'; +import LessonService from '../../../src/service/lesson.service'; const handler: NextApiHandler = async (req, res) => { const requestHandler = requestHandlers[req.method]; @@ -15,8 +13,45 @@ const getHandler: NextApiHandler = async (req, res) => { throw new Error('Not implemented.'); }; +const requiredValidation = (data: any, fieldName: string) => { + + const value = data ? data[fieldName] : null; + + if( !value || value === ''){ + throw new Error(`Field (${fieldName}) is required in the body`) + } + + return true; +} + const postHandler: NextApiHandler = async (req, res) => { - throw new Error('Not implemented.'); + try { + const data = JSON.parse(req?.body); + + console.log('DEBUG > postHandler > data = ', data); + + requiredValidation(data, 'name'); + requiredValidation(data, 'teacher'); + requiredValidation(data, 'day'); + requiredValidation(data, 'hour'); + requiredValidation(data, 'room'); + const { name, day, hour, room } = data; + const teacherId = Number(data.teacher); + const roomId = Number(data.room); + + const lessonService = new LessonService(); + const lessonId = await lessonService.create({teacherId, roomId, name, day, hour}); + + return res.status(HttpCode.CREATED).json(lessonId) + } catch (e) { + + console.error('error = ',e); + const message = `An unhandled error occurred in 'POST: ${req.url}: ${e?.message || 'Unknown error'}`; + console.error(message); + + res.statusMessage = message; + return res.status(HttpCode.INTERNAL_SERVER_ERROR).end(); + } }; const requestHandlers: HandlerCollection = { diff --git a/pages/api/teachers/summary.ts b/pages/api/teachers/summary.ts index 1cf5b90..070a1cb 100644 --- a/pages/api/teachers/summary.ts +++ b/pages/api/teachers/summary.ts @@ -11,9 +11,51 @@ const handler: NextApiHandler = async (req, res) => { return await requestHandler(req, res); }; +/* +* TODOS: the business logic of the summary should in teacher service; the controller is just for validating, formating the input and then pass args into the service +*/ const getHandler: NextApiHandler = async (req, res) => { + const prismaSql = Prisma.sql` - -- Complete task 3 here + SELECT + t.id, + t.title, + t."teacherName", + t."roomName", + l.lessons, + case + when l.lessons >= 10 then 'Full-time' + else + 'Casual' + end as type + FROM( + SELECT + t.id, + t.title, + t.name as "teacherName", + r."name" as "roomName", + count(l."roomId") as cnt, + ROW_NUMBER() OVER (PARTITION BY r."name" ORDER BY COUNT(*) DESC) as seqnum + FROM + "Lesson" l + JOIN "Teacher" t ON l."teacherId" = t.id + JOIN "Room" r ON l."roomId" = r.id + GROUP BY + t.id, t.title, t.name, r.name + ) t, + ( + SELECT + l."teacherId", + count(l.id) as lessons + FROM + "Lesson" l + GROUP BY + l."teacherId" + ) l + WHERE + 1 = 1 + AND t.seqnum = 1 + AND t.id = l."teacherId" `; const result = await prisma.$queryRaw(prismaSql); return res.status(HttpCode.OK).json({ result }); diff --git a/src/components/BookingForm.tsx b/src/components/BookingForm.tsx index 30093e6..770255e 100644 --- a/src/components/BookingForm.tsx +++ b/src/components/BookingForm.tsx @@ -1,50 +1,73 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { ThemeUICSSObject } from '@theme-ui/css'; import { Box, Heading } from '@theme-ui/components'; import { Label, Input, Select, Button } from 'theme-ui'; import { SessionDay } from '@/domain/session'; import { Teacher, Room } from '@prisma/client'; - const range = (length) => Array.from(Array(length).keys()).map((i) => ++i); const hours = range(24); const days: SessionDay[] = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - const labelStyle: ThemeUICSSObject = { display: 'block', mt: 4, mb: 2 }; - +const fetchData = async (url: string) => { + try { + let response = await fetch(url); + let json = await response.json(); + return { success: true, data: json.data }; + } catch (error) { + console.error(error); + return { success: false }; + } +} export const BookingForm = (): JSX.Element => { // Task 1 // Replace these temporary empty arrays with real data // Populate within the form - - const teachers: Teacher[] = []; - const rooms: Room[] = []; - + const [teachers, setTeachers] = useState([]); + const [rooms, setRooms] = useState([]); + const [loading, setLoading] = useState(false); + useEffect(() => { + (async () => { + setLoading(true); + const [teacherRes, roomRes] = await Promise.all([fetchData('/api/teachers'), fetchData('/api/rooms')]); + setLoading(false); + if (teacherRes.success) { + setTeachers(teacherRes.data); + } + if (roomRes.success) { + setRooms(roomRes.data); + } + })(); + }, []); const handleSubmit = async (e) => { e.preventDefault(); const form = e.target; const formData = new FormData(form); const formObj = {}; formData.forEach((value, key) => (formObj[key] = value)); - //Submit to booking endpoint try { const response = await fetch('/api/lessons', { method: 'POST', body: JSON.stringify(formObj), }); - const data = await response.json(); - - if (data.success) { + console.log(response); + if( response.status === 500 ){ + alert(`Booking failed: ${response.statusText}`); + }else if( response.status === 201 ){ alert('Booking successful'); - } else { - alert(`Booking failed: ${data.message}`); + }else{ + const data = await response.json(); + if (data.success) { + alert('Booking successful'); + } else { + alert(`Booking failed: ${data.message}`); + } } } catch (error) { - console.error(error); + console.error('e=',error); alert(`Error: ${error.message}`); } }; - return ( Schedule a new lesson @@ -58,6 +81,11 @@ export const BookingForm = (): JSX.Element => { ); }; - BookingForm.displayName = 'BookingForm'; diff --git a/src/service/base.service.ts b/src/service/base.service.ts new file mode 100644 index 0000000..7151124 --- /dev/null +++ b/src/service/base.service.ts @@ -0,0 +1,30 @@ +import { PrismaClient, Prisma } from '@prisma/client'; +import prisma from 'db/prisma'; + +/* +* TODOS: +* - implement the log service +* - create the config folder in /src; then create the config data for each envs: base.ts; dev.ts; stg.ts; prd.ts ... and have a helper to create the config for running env; +*. the confg the is set into baseservice +*/ +export default class BaseService{ + + protected _config; + protected _prisma = prisma; + protected _Prisma = Prisma; + protected _PrismaClient = PrismaClient; + serviceName: string; + + constructor(serviceName: string){ + this.serviceName = serviceName; + } + + protected _log(...args){ + console.log(`${this.serviceName} > INFO: `, ...args); + } + + protected _debug(...args){ + console.log(`${this.serviceName} > DEBUG: `, ...args); + } + +} diff --git a/src/service/lesson.service.ts b/src/service/lesson.service.ts new file mode 100644 index 0000000..8e1e34e --- /dev/null +++ b/src/service/lesson.service.ts @@ -0,0 +1,53 @@ +import BaseService from './base.service'; + +interface ILessonService{ + create({teacherId, roomId, name, day, hour}: {teacherId: number, roomId: number, name: string, day: number, hour: number}): Promise; +} + +/* +* TODOS: unit test +* implement IoC for easier unit test +*/ +export default class LessonService extends BaseService implements ILessonService{ + + constructor(){ + super('LessonService'); + } + + async create({teacherId, roomId, name, day, hour}: {teacherId: number, roomId: number, name: string, day: number, hour: number}){ + + const creatingSession = { day, startTime: hour }; + this._debug('creatingSession = ', creatingSession); + + let session = await this._prisma.session.findFirst({where : creatingSession }); + this._debug('foundSession = ', session); + + if( !session ){ + session = await this._prisma.session.create({ data: creatingSession }) + this._debug('createdSession = ', session); + } + + const foundTeacherLesson = await this._prisma.lesson.findFirst({ where: { teacherId, sessionId: session.id}}); + const foundRoomLesson = await this._prisma.lesson.findFirst({ where: { roomId, sessionId: session.id}}); + + this._debug('foundTeacherLesson = ', foundTeacherLesson); + this._debug('foundRoomLesson = ', foundRoomLesson); + + if( foundTeacherLesson ){ + const foundTeacher = await this._prisma.teacher.findFirst({ where: { id: teacherId}}); + throw new Error(`Teacher (${foundTeacher.title} ${foundTeacher.name}) has been booked for this session (${day} ${hour}:00)`); + } + + if( foundRoomLesson ){ + const foundRoom = await this._prisma.room.findFirst({ where: { id: roomId}}); + throw new Error(`Room (${foundRoom.name}) has been booked for this session (${day} ${hour}:00)`); + } + + const lesson = await this._prisma.lesson.create({ data: { name, teacherId , roomId, sessionId: session.id}}); + + this._log('created lesson = ', lesson); + + return lesson.id; + } + +}