Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phuong Nguyen 's technical excercise #1

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions pages/api/lessons/index.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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 = {
Expand Down
44 changes: 43 additions & 1 deletion pages/api/teachers/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>(prismaSql);
return res.status(HttpCode.OK).json({ result });
Expand Down
66 changes: 49 additions & 17 deletions src/components/BookingForm.tsx
Original file line number Diff line number Diff line change
@@ -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<Teacher[]>([]);
const [rooms, setRooms] = useState<Room[]>([]);
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 (
<Box p={8} sx={{ maxWidth: 900, mx: 'auto' }}>
<Heading mb={6}>Schedule a new lesson</Heading>
Expand All @@ -58,6 +81,11 @@ export const BookingForm = (): JSX.Element => {
</Label>
<Select name="teacher" id="teacher" mb={3}>
<option value="">Select an option...</option>
{teachers?.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{teacher.title} {teacher.name}
</option>
))}
</Select>
<Label sx={labelStyle} htmlFor="day">
Day
Expand Down Expand Up @@ -86,11 +114,15 @@ export const BookingForm = (): JSX.Element => {
</Label>
<Select name="room" id="room" mb={3}>
<option value="">Select a room...</option>
{rooms?.map((room) => (
<option key={room.id} value={room.id}>
{room.name}
</option>
))}
</Select>
<Button sx={{ mt: 6 }}>Submit</Button>
</Box>
</Box>
);
};

BookingForm.displayName = 'BookingForm';
30 changes: 30 additions & 0 deletions src/service/base.service.ts
Original file line number Diff line number Diff line change
@@ -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);
}

}
53 changes: 53 additions & 0 deletions src/service/lesson.service.ts
Original file line number Diff line number Diff line change
@@ -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<number>;
}

/*
* 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;
}

}