From 35a20acce6d6b5e9ccdee644fd23b326f2fd1dd9 Mon Sep 17 00:00:00 2001 From: Artem VV Date: Fri, 19 May 2023 21:15:27 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20=D1=81=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 39 +-- src/components/StudyItemsList.astro | 82 ++++++ src/components/TimetableEditor.astro | 258 ++++++++++++++++++ src/components/TimetableElement.astro | 30 ++ src/components/TimetableElementViewer.astro | 33 +++ src/components/TimetableSlotElement.astro | 120 ++++++++ .../TimetableSlotElementViewer.astro | 44 +++ src/components/TimetableViewer.astro | 51 ++++ src/db.ts | 153 +++++++++++ src/pages/timetable.astro | 32 +++ src/pages/ttapi/createStudyItem.ts | 39 +++ src/pages/ttapi/deleteStudyItem.ts | 39 +++ src/pages/ttapi/updateTimetable.ts | 44 +++ 13 files changed, 939 insertions(+), 25 deletions(-) create mode 100644 src/components/StudyItemsList.astro create mode 100644 src/components/TimetableEditor.astro create mode 100644 src/components/TimetableElement.astro create mode 100644 src/components/TimetableElementViewer.astro create mode 100644 src/components/TimetableSlotElement.astro create mode 100644 src/components/TimetableSlotElementViewer.astro create mode 100644 src/components/TimetableViewer.astro create mode 100644 src/pages/timetable.astro create mode 100644 src/pages/ttapi/createStudyItem.ts create mode 100644 src/pages/ttapi/deleteStudyItem.ts create mode 100644 src/pages/ttapi/updateTimetable.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c55a71e..4fb2688 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,34 +22,23 @@ model study_item { } model study_slot { - id Int @id(map: "pk_study_slot") @default(autoincrement()) - study_item_id Int? - study_item study_item? @relation(fields: [study_item_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "fk_study_slot_study_item") - timetable_timetable_slot_1Tostudy_slot timetable[] @relation("timetable_slot_1Tostudy_slot") - timetable_timetable_slot_2Tostudy_slot timetable[] @relation("timetable_slot_2Tostudy_slot") - timetable_timetable_slot_3Tostudy_slot timetable[] @relation("timetable_slot_3Tostudy_slot") - timetable_timetable_slot_4Tostudy_slot timetable[] @relation("timetable_slot_4Tostudy_slot") - timetable_timetable_slot_5Tostudy_slot timetable[] @relation("timetable_slot_5Tostudy_slot") - timetable_timetable_slot_6Tostudy_slot timetable[] @relation("timetable_slot_6Tostudy_slot") + id Int @id(map: "pk_study_slot") @default(autoincrement()) + study_item_id Int + where String + studentsGroup String + position Int + study_item study_item @relation(fields: [study_item_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_study_slot_study_item") + timetable timetable? @relation(fields: [timetableId], references: [id]) + timetableId Int? } model timetable { - id Int @id(map: "pk_timetable") @default(autoincrement()) - slot_1 Int? - slot_2 Int? - slot_3 Int? - slot_4 Int? - slot_5 Int? - slot_6 Int? - day Int @db.SmallInt - teacher Int - study_slot_timetable_slot_1Tostudy_slot study_slot? @relation("timetable_slot_1Tostudy_slot", fields: [slot_1], references: [id], map: "fk_timetable_study_slot_1") - study_slot_timetable_slot_2Tostudy_slot study_slot? @relation("timetable_slot_2Tostudy_slot", fields: [slot_2], references: [id], map: "fk_timetable_study_slot_2") - study_slot_timetable_slot_3Tostudy_slot study_slot? @relation("timetable_slot_3Tostudy_slot", fields: [slot_3], references: [id], map: "fk_timetable_study_slot_3") - study_slot_timetable_slot_4Tostudy_slot study_slot? @relation("timetable_slot_4Tostudy_slot", fields: [slot_4], references: [id], map: "fk_timetable_study_slot_4") - study_slot_timetable_slot_5Tostudy_slot study_slot? @relation("timetable_slot_5Tostudy_slot", fields: [slot_5], references: [id], map: "fk_timetable_study_slot_5") - study_slot_timetable_slot_6Tostudy_slot study_slot? @relation("timetable_slot_6Tostudy_slot", fields: [slot_6], references: [id], map: "fk_timetable_study_slot_6") - users users @relation(fields: [teacher], references: [id], onDelete: Cascade, map: "fk_timetable_users_teacher") + id Int @id(map: "pk_timetable") @default(autoincrement()) + day Int @db.SmallInt + odd Boolean + slots study_slot[] + teacher users @relation(fields: [teacherId], references: [id]) + teacherId Int } model users { diff --git a/src/components/StudyItemsList.astro b/src/components/StudyItemsList.astro new file mode 100644 index 0000000..dd0b4f5 --- /dev/null +++ b/src/components/StudyItemsList.astro @@ -0,0 +1,82 @@ +--- +import { getStudyItems } from "../db"; + +const studyItems = await getStudyItems(); +--- + +
+ + {studyItems.map((si) => )} + +
+
+

Предметы

+
+
+ + +
+
+ +
+
+
+ { + studyItems.map((item) => ( +
+

{item.title}

+ +
+ )) + } + {studyItems.length === 0 &&
Предметов нет
} +
+
+
+
+
+ + diff --git a/src/components/TimetableEditor.astro b/src/components/TimetableEditor.astro new file mode 100644 index 0000000..2cf89a5 --- /dev/null +++ b/src/components/TimetableEditor.astro @@ -0,0 +1,258 @@ +--- +import TimetableElement from "./TimetableElement.astro"; + +import type { users, timetable } from "@prisma/client"; +import { getUserFromLogin, searchUsers, getTimetable, createTimetable } from "../db"; + +const allUsers = await searchUsers({ + isAdmin: false, +}); + +let editedUser: users | null = null; +const sLogin = Astro.url.searchParams.get("login"); +if (sLogin !== null) { + editedUser = await getUserFromLogin(sLogin); +} + +let oddTT: timetable[] = []; +let evenTT: timetable[] = []; +if (editedUser !== null) { + oddTT = await getTimetable(editedUser.id, true); + if (oddTT.length === 0) { + await createTimetable(editedUser.id, true); + oddTT = await getTimetable(editedUser.id, true); + } + evenTT = await getTimetable(editedUser.id, false); + if (evenTT.length === 0) { + await createTimetable(editedUser.id, false); + evenTT = await getTimetable(editedUser.id, false); + } +} +--- + +
+ { + editedUser === null && ( +
+ + + + {allUsers.map((user) => ( + + ))} + + +
+ ) + } + { + editedUser !== null && ( + <> +
+
+
Нечётная неделя
+
+ + + + + + +
+
+
+
Чётная неделя
+
+ + + + + + +
+
+
+
+ + <> + + + + +
+ + + <> + + + + + + + ) + } +
+ + + + diff --git a/src/components/TimetableElement.astro b/src/components/TimetableElement.astro new file mode 100644 index 0000000..26bef36 --- /dev/null +++ b/src/components/TimetableElement.astro @@ -0,0 +1,30 @@ +--- +import TimetableSlotElement from "./TimetableSlotElement.astro"; + +import type { timetable } from "@prisma/client"; +import { getTimetableSlots } from "../db"; + +export interface Props { + tt: timetable; + position: number; +} + +const { tt, position } = Astro.props; + +const dayNames = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; + +const slots = await getTimetableSlots(tt.id); +--- + +
+
+
{dayNames[tt.day]}
+
+
+ { + [0, 1, 2, 3, 4, 5].map((i) => ( + slot.position == i) ?? null} position={i} day={tt.day} isOdd={tt.odd} /> + )) + } +
+
diff --git a/src/components/TimetableElementViewer.astro b/src/components/TimetableElementViewer.astro new file mode 100644 index 0000000..a35bd1e --- /dev/null +++ b/src/components/TimetableElementViewer.astro @@ -0,0 +1,33 @@ +--- +import TimetableSlotElementViewer from "./TimetableSlotElementViewer.astro"; + +import type { timetable } from "@prisma/client"; +import { getTimetableSlots } from "../db"; + +export interface Props { + tt: timetable; + position: number; +} + +const { tt, position } = Astro.props; + +const dayNames = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]; + +const slots = await getTimetableSlots(tt.id); +const none = slots.length == 0; +--- + +{ + !none && ( +
+
+
{dayNames[tt.day]}
+
+
+ {[0, 1, 2, 3, 4, 5].map((i) => ( + slot.position == i) ?? null} position={i} day={tt.day} isOdd={tt.odd} /> + ))} +
+
+ ) +} diff --git a/src/components/TimetableSlotElement.astro b/src/components/TimetableSlotElement.astro new file mode 100644 index 0000000..a2c7f93 --- /dev/null +++ b/src/components/TimetableSlotElement.astro @@ -0,0 +1,120 @@ +--- +import type { study_slot, study_item } from "@prisma/client"; +import { getStudyItem } from "../db"; + +export interface Props { + ss: study_slot | null; + position: number; + day: number; + isOdd: boolean; +} + +const { ss, position, day, isOdd } = Astro.props; + +const positionToTime = ["08:30-10:05", "10:15-11:50", "12:00-13:35", "14:10-15:45", "15:55-17:30", "17:40-19:15"]; + +let studyItem: study_item | null = null; +if (ss !== null) { + studyItem = await getStudyItem(ss.id); +} +--- + +
+
+ { + ss === null && ( + + ) + } + { + ss !== null && ( + + ) + } + +
{positionToTime[position]}
+
+ + + + + + + + +
+
+ { + ss !== null && ( + <> +
+
+
{studyItem!.title}
+
{positionToTime[position]}
+
Где: {ss.where}
+
Группа(ы): {ss.studentsGroup}
+
+ + ) + } +
+ + diff --git a/src/components/TimetableSlotElementViewer.astro b/src/components/TimetableSlotElementViewer.astro new file mode 100644 index 0000000..aa7b0b2 --- /dev/null +++ b/src/components/TimetableSlotElementViewer.astro @@ -0,0 +1,44 @@ +--- +import type { study_slot, study_item } from "@prisma/client"; +import { getStudyItem } from "../db"; + +export interface Props { + ss: study_slot | null; + position: number; + day: number; + isOdd: boolean; +} + +const { ss, position, day, isOdd } = Astro.props; + +const positionToTime = ["08:30-10:05", "10:15-11:50", "12:00-13:35", "14:10-15:45", "15:55-17:30", "17:40-19:15"]; + +let studyItem: study_item | null = null; +if (ss !== null) { + studyItem = await getStudyItem(ss.id); +} +--- + +{ + ss !== null && ( +
+
+
{studyItem!.title}
+
{positionToTime[position]}
+
Где: {ss.where}
+
Группа(ы): {ss.studentsGroup}
+
+
+ ) +} +{ + /* + ss === null && ( +
+
+
Свободно
+
{positionToTime[position]}
+
+
+ )*/ +} diff --git a/src/components/TimetableViewer.astro b/src/components/TimetableViewer.astro new file mode 100644 index 0000000..31e1cff --- /dev/null +++ b/src/components/TimetableViewer.astro @@ -0,0 +1,51 @@ +--- +import TimetableElementViewer from "./TimetableElementViewer.astro"; + +import type { users, timetable } from "@prisma/client"; +import { getTimetable, createTimetable } from "../db"; + +export interface Props { + user: users; +} + +const { user } = Astro.props; + +let oddTT: timetable[] = await getTimetable(user.id, true); +let evenTT: timetable[] = await getTimetable(user.id, false); + +if (oddTT.length === 0) { + await createTimetable(user.id, true); + oddTT = await getTimetable(user.id, true); +} +if (evenTT.length === 0) { + await createTimetable(user.id, false); + evenTT = await getTimetable(user.id, false); +} +--- + +
+
+
+
Нечётная неделя
+
+ + + + + + +
+
+
+
Чётная неделя
+
+ + + + + + +
+
+
+
diff --git a/src/db.ts b/src/db.ts index b37857b..3454859 100644 --- a/src/db.ts +++ b/src/db.ts @@ -218,3 +218,156 @@ export async function searchUsers(params: { login?: string; isAdmin?: boolean; f }); return users; } + +export async function getStudyItems() { + const items = await client.study_item.findMany({ + orderBy: { + id: "asc", + }, + }); + return items; +} + +export async function createStudyItem(title: string) { + const item = await client.study_item.create({ + data: { + title: title, + }, + }); + return item !== null; +} + +export async function deleteStudyItem(id: number) { + const result = await client.study_item.delete({ + where: { + id: id, + }, + }); + return result !== null; +} + +export async function getTimetable(userId: number, isOdd: boolean) { + const tt = await client.timetable.findMany({ + where: { + teacherId: userId, + odd: isOdd, + }, + orderBy: { + day: "asc", + }, + }); + return tt; +} + +export async function createTimetable(userId: number, isOdd: boolean) { + const tt = await client.timetable.createMany({ + data: [ + { day: 0, odd: isOdd, teacherId: userId }, + { day: 1, odd: isOdd, teacherId: userId }, + { day: 2, odd: isOdd, teacherId: userId }, + { day: 3, odd: isOdd, teacherId: userId }, + { day: 4, odd: isOdd, teacherId: userId }, + { day: 5, odd: isOdd, teacherId: userId }, + ], + }); + return tt.count === 6; +} + +export async function getTimetableSlots(ttId: number) { + const slots = await client.study_slot.findMany({ + where: { + timetableId: ttId, + }, + orderBy: { + position: "asc", + }, + }); + return slots; +} + +export async function getStudyItem(ttSlotId: number) { + const slot = await client.study_slot.findFirst({ + where: { + id: ttSlotId, + }, + }); + const item = await client.study_item.findFirst({ + where: { + id: slot!.study_item_id, + }, + }); + return item; +} + +export type UpdateTTElementModel = { + isWindow: boolean; + position: number; + day: number; + studyItem: number; + slotPlace: string; + slotGroup: string; +}; + +export async function updateUserTimetable(userLogin: string, oddUpdate: UpdateTTElementModel[][], evenUpdate: UpdateTTElementModel[][]) { + const user = await getUserFromLogin(userLogin); + const oddTT = await client.timetable.findMany({ + where: { + teacherId: user!.id, + odd: true, + }, + }); + const evenTT = await client.timetable.findMany({ + where: { + teacherId: user!.id, + odd: false, + }, + }); + for (let odt of oddTT) { + await client.study_slot.deleteMany({ + where: { + timetableId: odt.id, + }, + }); + } + for (let edt of evenTT) { + await client.study_slot.deleteMany({ + where: { + timetableId: edt.id, + }, + }); + } + await client.study_slot.createMany({ + data: oddUpdate + .map((e) => { + return e + .filter((e) => !e.isWindow) + .map((el) => { + return { + timetableId: oddTT.find((e) => e.day === el.day)!.id, + position: el.position, + study_item_id: el.studyItem, + where: el.slotPlace, + studentsGroup: el.slotGroup, + }; + }); + }) + .reduce((acc, val) => acc.concat(val), []), + }); + await client.study_slot.createMany({ + data: evenUpdate + .map((e) => { + return e + .filter((e) => !e.isWindow) + .map((el) => { + return { + timetableId: evenTT.find((e) => e.day === el.day)!.id, + position: el.position, + study_item_id: el.studyItem, + where: el.slotPlace, + studentsGroup: el.slotGroup, + }; + }); + }) + .reduce((acc, val) => acc.concat(val), []), + }); +} diff --git a/src/pages/timetable.astro b/src/pages/timetable.astro new file mode 100644 index 0000000..e4bfa68 --- /dev/null +++ b/src/pages/timetable.astro @@ -0,0 +1,32 @@ +--- +import Layout from "../layouts/Layout.astro"; +import Navbar from "../components/Navbar.astro"; +import TimetableViewer from "../components/TimetableViewer.astro"; +import StudyItemsList from "../components/StudyItemsList.astro"; +import TimetableEditor from "../components/TimetableEditor.astro"; + +import { getUserSession, getSessionUser } from "../db"; + +if (Astro.cookies.has("session")) { + const sessId = Astro.cookies.get("session").value!; + const dbSess = await getUserSession(sessId); + if (dbSess === null) { + Astro.cookies.delete("session"); + return Astro.redirect("/login"); + } +} else { + return Astro.redirect("/login"); +} + +const sessId = Astro.cookies.get("session").value!; +const user = (await getSessionUser(sessId))!; +--- + + +
+ + {user.is_admin && } + {user.is_admin && } + {!user.is_admin && } +
+
diff --git a/src/pages/ttapi/createStudyItem.ts b/src/pages/ttapi/createStudyItem.ts new file mode 100644 index 0000000..0f9cabf --- /dev/null +++ b/src/pages/ttapi/createStudyItem.ts @@ -0,0 +1,39 @@ +import type { APIContext } from "astro"; +import { createStudyItem, getSessionUser } from "../../db"; +import { Prisma } from "@prisma/client"; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + if (!user.is_admin) { + throw new Error("Доступно только администраторам"); + } + + const formData = await request.formData(); + const studyItemName = formData.get("studyItemName"); + if (studyItemName === null) { + throw new Error("Не предоставлены данные создания предмета"); + } + + await createStudyItem(studyItemName.toString()); + + response.ok = true; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +} diff --git a/src/pages/ttapi/deleteStudyItem.ts b/src/pages/ttapi/deleteStudyItem.ts new file mode 100644 index 0000000..257cb7c --- /dev/null +++ b/src/pages/ttapi/deleteStudyItem.ts @@ -0,0 +1,39 @@ +import type { APIContext } from "astro"; +import { deleteStudyItem, getSessionUser } from "../../db"; +import { Prisma } from "@prisma/client"; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + if (!user.is_admin) { + throw new Error("Доступно только администраторам"); + } + + const formData = await request.formData(); + const itemId = formData.get("itemId"); + if (itemId === null) { + throw new Error("Не предоставлены данные создания предмета"); + } + + await deleteStudyItem(parseInt(itemId.toString())); + + response.ok = true; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +} diff --git a/src/pages/ttapi/updateTimetable.ts b/src/pages/ttapi/updateTimetable.ts new file mode 100644 index 0000000..5f31e5b --- /dev/null +++ b/src/pages/ttapi/updateTimetable.ts @@ -0,0 +1,44 @@ +import type { APIContext } from "astro"; +import { getSessionUser, updateUserTimetable, UpdateTTElementModel } from "../../db"; +import { Prisma } from "@prisma/client"; + +type UpdateTTModel = { + login: string; + odd: UpdateTTElementModel[][]; + even: UpdateTTElementModel[][]; +}; + +export async function post({ request, cookies }: APIContext) { + const response: { ok: boolean; reason?: string } = { + ok: true, + }; + try { + const sessId = cookies.get("session").value!; + const user = (await getSessionUser(sessId))!; + if (!user.is_admin) { + throw new Error("Доступно только администраторам"); + } + + const data: UpdateTTModel = await request.json(); + if (data === null || data.login === null || data.odd === null || data.even === null) { + throw new Error("Не предоставлены данные обновления расписания"); + } + + await updateUserTimetable(data.login, data.odd, data.even); + + response.ok = true; + } catch (e: any) { + response.ok = false; + if (e instanceof Prisma.PrismaClientKnownRequestError) { + response.reason = `Неизвестная ошибка базы данных. Код ${e.code}`; + } else if (e instanceof Error) { + response.reason = e.message.trim(); + } else { + response.reason = e.toString().trim(); + } + } + + return { + body: JSON.stringify(response), + }; +}