diff --git a/Dockerfile b/Dockerfile index 5d335c2..a7776d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,7 @@ RUN npm install EXPOSE 3000 # Run the application -CMD echo "Waiting for database to be ready..." && \ - echo "Running Prisma commands..." && \ +CMD echo "Running Prisma commands..." && \ npx prisma generate && \ npx prisma db push && \ npx @snaplet/seed sync && \ diff --git a/package-lock.json b/package-lock.json index 7488dd7..57c0d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "@types/react-dom": "^18.0.0", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", - "eslint": "^8.57.1", + "eslint": "^8.57.0", "eslint-config-next": "14.2.3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -1461,6 +1461,134 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.14.tgz", + "integrity": "sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.14.tgz", + "integrity": "sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.14.tgz", + "integrity": "sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.14.tgz", + "integrity": "sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.14.tgz", + "integrity": "sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.14.tgz", + "integrity": "sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.14.tgz", + "integrity": "sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.14.tgz", + "integrity": "sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5206,6 +5334,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/package.json b/package.json index f7bde2a..f7d618d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "prisma generate && prisma migrate && next build", "start": "next start", "lint": "next lint", "lint:fix": "eslint . --ext js,jsx,ts,tsx --fix", @@ -20,9 +20,9 @@ "@chakra-ui/theme-tools": "^2.0.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", - "@faker-js/faker": "^8.4.1", "@mui/material": "^5.15.20", "@mui/system": "^5.15.20", + "@faker-js/faker": "^8.4.1", "@prisma/client": "^5.17.0", "dotenv": "^16.4.5", "framer-motion": "^6.3.0", @@ -30,8 +30,8 @@ "next-auth": "^5.0.0-beta.19", "nodemailer": "^6.9.14", "pnpm": "^9.4.0", - "postgres": "^3.4.4", "prisma": "^5.16.0", + "postgres": "^3.4.4", "react": "^18.2.0", "react-calendar": "^5.0.0", "react-dom": "^18.2.0" @@ -44,7 +44,7 @@ "@types/react-dom": "^18.0.0", "@typescript-eslint/eslint-plugin": "^8.8.0", "@typescript-eslint/parser": "^8.8.0", - "eslint": "^8.57.1", + "eslint": "^8.57.0", "eslint-config-next": "14.2.3", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/prisma/seed/.snaplet/dataModel.json b/prisma/seed/.snaplet/dataModel.json index e68835f..a96aa4c 100644 --- a/prisma/seed/.snaplet/dataModel.json +++ b/prisma/seed/.snaplet/dataModel.json @@ -23,6 +23,20 @@ "isId": true, "maxLength": null }, + { + "id": "public.Absence.absentTeacherId", + "name": "absentTeacherId", + "columnName": "absentTeacherId", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, { "id": "public.Absence.lessonDate", "name": "lessonDate", @@ -65,20 +79,6 @@ "isId": false, "maxLength": null }, - { - "id": "public.Absence.absentTeacherId", - "name": "absentTeacherId", - "columnName": "absentTeacherId", - "type": "int4", - "isRequired": true, - "kind": "scalar", - "isList": false, - "isGenerated": false, - "sequence": false, - "hasDefaultValue": false, - "isId": false, - "maxLength": null - }, { "id": "public.Absence.substituteTeacherId", "name": "substituteTeacherId", @@ -561,6 +561,132 @@ "nullNotDistinct": false } ] + }, + "_prisma_migrations": { + "id": "public._prisma_migrations", + "schemaName": "public", + "tableName": "_prisma_migrations", + "fields": [ + { + "id": "public._prisma_migrations.id", + "name": "id", + "columnName": "id", + "type": "varchar", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": true, + "maxLength": 36 + }, + { + "id": "public._prisma_migrations.checksum", + "name": "checksum", + "columnName": "checksum", + "type": "varchar", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": 64 + }, + { + "id": "public._prisma_migrations.finished_at", + "name": "finished_at", + "columnName": "finished_at", + "type": "timestamptz", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public._prisma_migrations.migration_name", + "name": "migration_name", + "columnName": "migration_name", + "type": "varchar", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": 255 + }, + { + "id": "public._prisma_migrations.logs", + "name": "logs", + "columnName": "logs", + "type": "text", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public._prisma_migrations.rolled_back_at", + "name": "rolled_back_at", + "columnName": "rolled_back_at", + "type": "timestamptz", + "isRequired": false, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": false, + "isId": false, + "maxLength": null + }, + { + "id": "public._prisma_migrations.started_at", + "name": "started_at", + "columnName": "started_at", + "type": "timestamptz", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + }, + { + "id": "public._prisma_migrations.applied_steps_count", + "name": "applied_steps_count", + "columnName": "applied_steps_count", + "type": "int4", + "isRequired": true, + "kind": "scalar", + "isList": false, + "isGenerated": false, + "sequence": false, + "hasDefaultValue": true, + "isId": false, + "maxLength": null + } + ], + "uniqueConstraints": [ + { + "name": "_prisma_migrations_pkey", + "fields": ["id"], + "nullNotDistinct": false + } + ] } }, "enums": { diff --git a/src/components/Calendar/CalendarStyles.tsx b/src/components/Calendar/CalendarStyles.tsx new file mode 100644 index 0000000..de1c350 --- /dev/null +++ b/src/components/Calendar/CalendarStyles.tsx @@ -0,0 +1,48 @@ +import { Global } from '@emotion/react'; + +export const CalendarStyles = () => ( + abbr { + position: absolute; + top: 4px; + left: 4px; + z-index: 1; + } + `} + /> +); diff --git a/src/components/Calendar/DayView.tsx b/src/components/Calendar/DayView.tsx new file mode 100644 index 0000000..3efcd97 --- /dev/null +++ b/src/components/Calendar/DayView.tsx @@ -0,0 +1,41 @@ +import { Box, Text } from '@chakra-ui/react'; +import { Absence } from '../../types/absence'; + +interface DayViewProps { + date: Date; + absences: Absence[]; + onDelete: (id: number) => Promise; +} + +const DayView: React.FC = ({ date, absences, onDelete }) => { + return ( + + {date.toDateString()} + {absences + .filter( + (absence) => absence.lessonDate.toDateString() === date.toDateString() + ) + .map((absence, index) => ( + + Reason of Absence: {absence.reasonOfAbsence} + onDelete(absence.id)} + _hover={{ backgroundColor: 'red.600' }} + _active={{ backgroundColor: 'red.700' }} + > + Delete + + + ))} + + ); +}; + +export default DayView; diff --git a/src/components/Calendar/TileContent.tsx b/src/components/Calendar/TileContent.tsx new file mode 100644 index 0000000..2881340 --- /dev/null +++ b/src/components/Calendar/TileContent.tsx @@ -0,0 +1,89 @@ +import { Box, VStack } from '@chakra-ui/react'; +import { Absence } from '../../types/absence'; + +interface TileContentProps { + date: Date; + view: string; + absences: Absence[]; + onAddButtonClick: (date: Date) => void; + onDelete: (id: number) => Promise; +} + +const TileContent: React.FC = ({ + date, + view, + absences, + onAddButtonClick, + onDelete, +}) => { + if (view !== 'month') return null; + + const dayAbsences = absences.filter( + (absence) => absence.lessonDate.toDateString() === date.toDateString() + ); + + return ( + + onAddButtonClick(date)} + position="absolute" + top="2px" + right="2px" + zIndex="1" + minW="20px" + height="20px" + p="0" + bg="green.400" + color="white" + borderRadius="full" + fontSize="sm" + textAlign="center" + display="flex" + alignItems="center" + justifyContent="center" + cursor="pointer" + _hover={{ bg: 'green.500' }} + > + + + + + {dayAbsences.map((absence, index) => ( + + onDelete(absence.id)} + cursor="pointer" + > + Delete + + + ))} + + + ); +}; + +export default TileContent; diff --git a/src/components/Calendar/WeekView.tsx b/src/components/Calendar/WeekView.tsx new file mode 100644 index 0000000..a5d5070 --- /dev/null +++ b/src/components/Calendar/WeekView.tsx @@ -0,0 +1,61 @@ +import { Box, Text } from '@chakra-ui/react'; +import { Absence } from '../../types/absence'; + +interface WeekViewProps { + date: Date; + absences: Absence[]; + onDelete: (id: number) => Promise; +} + +const WeekView: React.FC = ({ date, absences, onDelete }) => { + const startOfWeek = new Date(date); + startOfWeek.setDate(date.getDate() - date.getDay()); + + const weekDays = Array.from({ length: 7 }, (_, i) => { + const day = new Date(startOfWeek); + day.setDate(startOfWeek.getDate() + i); + return day; + }); + + return ( + + {weekDays.map((day) => ( + + {day.toDateString()} + {absences + .filter( + (absence) => + absence.lessonDate.toDateString() === day.toDateString() + ) + .map((absence, index) => ( + + Reason of Absence: {absence.reasonOfAbsence} + onDelete(absence.id)} + _hover={{ backgroundColor: 'red.600' }} + _active={{ backgroundColor: 'red.700' }} + > + Delete + + + ))} + + ))} + + ); +}; + +export default WeekView; diff --git a/src/components/InputForm.tsx b/src/components/InputForm.tsx new file mode 100644 index 0000000..0b7a7c6 --- /dev/null +++ b/src/components/InputForm.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; +import { + Box, + Button, + Input, + FormControl, + FormLabel, + Text, +} from '@chakra-ui/react'; + +interface Absence { + id?: number; + lessonDate: Date; + lessonPlan: string | null; + reasonOfAbsence: string; + absentTeacherId: number; + substituteTeacherId: number | null; + locationId: number; + subjectId: number; +} + +interface InputFormProps { + initialDate: Date; + onClose: () => void; + onAddAbsence: (newAbsence: Absence) => Promise; +} + +const InputForm: React.FC = ({ + initialDate, + onClose, + onAddAbsence, +}) => { + const [lessonPlan, setLessonPlan] = useState(''); + const [reasonOfAbsence, setReasonOfAbsence] = useState(''); + const [absentTeacherId, setAbsentTeacherId] = useState(''); + const [substituteTeacherId, setSubstituteTeacherId] = useState(''); + const [locationId, setLocationId] = useState(''); + const [subjectId, setSubjectId] = useState(''); + const [error, setError] = useState(''); + + const handleAddAbsence = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + const newAbsence: Absence = { + lessonDate: initialDate, + lessonPlan: lessonPlan || null, + reasonOfAbsence, + absentTeacherId: parseInt(absentTeacherId, 10), + substituteTeacherId: substituteTeacherId + ? parseInt(substituteTeacherId, 10) + : null, + locationId: parseInt(locationId, 10), + subjectId: parseInt(subjectId, 10), + }; + + // Pass the newAbsence object to onAddAbsence and await the response + const success = await onAddAbsence(newAbsence); + + if (success) { + // Clear the form on successful submission + setLessonPlan(''); + setReasonOfAbsence(''); + setAbsentTeacherId(''); + setSubstituteTeacherId(''); + setLocationId(''); + setSubjectId(''); + onClose(); + } else { + setError('Invalid input. Please enter correct details.'); + } + } catch (err) { + // In case of any error, set the error message + setError(err); + } + }; + + return ( + + + Lesson Plan + setLessonPlan(e.target.value)} + color="black" + /> + + + Reason of Absence + setReasonOfAbsence(e.target.value)} + required + color="black" + /> + + + Absent Teacher ID + setAbsentTeacherId(e.target.value)} + required + color="black" + /> + + + Substitute Teacher ID + setSubstituteTeacherId(e.target.value)} + color="black" + /> + + + Location ID + setLocationId(e.target.value)} + required + color="black" + /> + + + Subject ID + setSubjectId(e.target.value)} + required + color="black" + /> + + {error && ( + + {error} + + )} + + + ); +}; + +export default InputForm; diff --git a/src/pages/api/absence.ts b/src/pages/api/absence.ts new file mode 100644 index 0000000..887f916 --- /dev/null +++ b/src/pages/api/absence.ts @@ -0,0 +1,64 @@ +import { PrismaClient } from '@prisma/client'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +const prisma = new PrismaClient(); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === 'GET') { + try { + const absences = await prisma.absence.findMany(); + res.status(200).json({ absences }); + } catch (error) { + res + .status(500) + .json({ error: 'Failed to fetch absences', message: error.message }); + } + } else if (req.method === 'POST') { + const { + lessonDate, + lessonPlan, + reasonOfAbsence, + absentTeacherId, + substituteTeacherId, + locationId, + subjectId, + } = req.body; + try { + const newAbsence = await prisma.absence.create({ + data: { + lessonDate, + lessonPlan, + reasonOfAbsence, + absentTeacherId, + substituteTeacherId, + locationId, + subjectId, + }, + }); + + res.status(200).json({ newAbsence }); + } catch (error) { + res + .status(500) + .json({ error: 'Failed to add absence', message: error.message }); + } + } else if (req.method === 'DELETE') { + const { id } = req.body; + try { + await prisma.absence.delete({ + where: { id }, + }); + res.status(200).json({ message: 'Absence deleted' }); + } catch (error) { + res + .status(500) + .json({ error: 'Failed to delete absence', message: error.message }); + } + } else { + res.setHeader('Allow', ['GET', 'POST', 'DELETE']); + res.status(405).end(`Method ${req.method} Not Allowed`); + } +} diff --git a/src/pages/calendar.tsx b/src/pages/calendar.tsx index 08c6383..2319484 100644 --- a/src/pages/calendar.tsx +++ b/src/pages/calendar.tsx @@ -1,32 +1,228 @@ -import { Calendar as ReactCalendar } from 'react-calendar'; +import { useState, useEffect, useCallback } from 'react'; +import dynamic from 'next/dynamic'; import 'react-calendar/dist/Calendar.css'; +import { + Box, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + Select, + Text, + Flex, +} from '@chakra-ui/react'; +import { Absence, FetchAbsenceResponse } from '../types/absence'; +import InputForm from '../components/InputForm'; +import TileContent from '../components/Calendar/TileContent'; +import WeekView from '../components/Calendar/WeekView'; +import DayView from '../components/Calendar/DayView'; +import { CalendarStyles } from '../components/Calendar/CalendarStyles'; -import { Container, Text } from '@chakra-ui/react'; +const Calendar = dynamic(() => import('react-calendar'), { ssr: false }); -import React, { useState } from 'react'; +function CalendarView() { + const [absences, setAbsences] = useState([]); + const [value, setValue] = useState(new Date()); + const [isFormOpen, setIsFormOpen] = useState(false); + const [formDate, setFormDate] = useState(null); + const [view, setView] = useState('month'); -type ValuePiece = Date | null; + const fetchAbsences = useCallback(async () => { + const res = await fetch('/api/absence'); + const data: FetchAbsenceResponse = await res.json(); + setAbsences( + data.absences.map((absence) => ({ + ...absence, + lessonDate: new Date(absence.lessonDate), + })) + ); + }, []); -type Value = ValuePiece | [ValuePiece, ValuePiece]; + useEffect(() => { + fetchAbsences(); + }, [fetchAbsences]); -const Calendar = () => { - const [value, onChange] = useState(new Date()); - const [clickedDate, setClickedDate] = useState(null); + const onAddButtonClick = (date: Date) => { + setFormDate(date); + setIsFormOpen(true); + }; + + const handleAbsenceDelete = async (id: number) => { + try { + const response = await fetch('/api/absence', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ id }), + }); + if (response.ok) { + setAbsences((prevAbsences) => + prevAbsences.filter((absence) => absence.id !== id) + ); + } else { + console.error( + 'Failed to delete absence with id ${id}. Status: ${response.status}' + ); + } + } catch (error) { + console.error('Error deleting absence:', error); + } + }; + + const addAbsence = async (absence: Absence): Promise => { + try { + const response = await fetch('/api/absence', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(absence), + }); + + if (response.ok) { + const newAbsence = await response.json(); + const addedAbsence = { + ...newAbsence.newAbsence, + lessonDate: new Date(newAbsence.newAbsence.lessonDate), + }; + setAbsences([...absences, addedAbsence]); + setIsFormOpen(false); + return addedAbsence; + } else { + const errorResponse = await response.json(); + console.error('Error response:', response.status, errorResponse); + return null; + } + } catch (error) { + console.error('Error adding absence:', error); + return null; + } + }; + + const onDelete = async (id: number) => { + try { + await handleAbsenceDelete(id); + setAbsences((prevAbsences) => + prevAbsences.filter((absence) => absence.id !== id) + ); + } catch (error) { + console.error('Failed to delete absence:', error); + } + }; + + const handleDateChange = (newValue: Date | [Date, Date]) => { + if (newValue) { + setValue(newValue); + } + }; - const onClickDay = (value: ValuePiece) => { - setClickedDate(value); + const renderCalendar = () => { + switch (view) { + case 'month': + return ( + + ( + + )} + tileClassName="calendar-tile" + className="react-calendar" + showNeighboringMonth={false} + /> + + ); + case 'week': + return ( + + ); + case 'day': + return ( + + ); + default: + return null; + } }; return ( - - - Clicked date: {clickedDate?.toDateString()} - + + + + + + View: + + + + + + {renderCalendar()} + + {isFormOpen && formDate && ( + setIsFormOpen(false)}> + + + Add Absence + + + setIsFormOpen(false)} + onAddAbsence={addAbsence} + /> + + + + )} + ); -}; +} -export default Calendar; +export default CalendarView; diff --git a/src/types/absence.ts b/src/types/absence.ts new file mode 100644 index 0000000..75819aa --- /dev/null +++ b/src/types/absence.ts @@ -0,0 +1,16 @@ +export interface Absence { + id: number; + title: string; + lessonDate: Date; + lessonPlan: string | null; + reasonOfAbsence: string; + absentTeacherId: number; + substituteTeacherId: number | null; + subjectId: number; + locationId: number; + newAbsence?: Omit; +} + +export interface FetchAbsenceResponse { + absences: Absence[]; +}