Добавил работу с расписанием
This commit is contained in:
parent
e9b8b44d1b
commit
35a20acce6
13 changed files with 939 additions and 25 deletions
|
|
@ -23,33 +23,22 @@ model study_item {
|
||||||
|
|
||||||
model study_slot {
|
model study_slot {
|
||||||
id Int @id(map: "pk_study_slot") @default(autoincrement())
|
id Int @id(map: "pk_study_slot") @default(autoincrement())
|
||||||
study_item_id Int?
|
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")
|
where String
|
||||||
timetable_timetable_slot_1Tostudy_slot timetable[] @relation("timetable_slot_1Tostudy_slot")
|
studentsGroup String
|
||||||
timetable_timetable_slot_2Tostudy_slot timetable[] @relation("timetable_slot_2Tostudy_slot")
|
position Int
|
||||||
timetable_timetable_slot_3Tostudy_slot timetable[] @relation("timetable_slot_3Tostudy_slot")
|
study_item study_item @relation(fields: [study_item_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_study_slot_study_item")
|
||||||
timetable_timetable_slot_4Tostudy_slot timetable[] @relation("timetable_slot_4Tostudy_slot")
|
timetable timetable? @relation(fields: [timetableId], references: [id])
|
||||||
timetable_timetable_slot_5Tostudy_slot timetable[] @relation("timetable_slot_5Tostudy_slot")
|
timetableId Int?
|
||||||
timetable_timetable_slot_6Tostudy_slot timetable[] @relation("timetable_slot_6Tostudy_slot")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model timetable {
|
model timetable {
|
||||||
id Int @id(map: "pk_timetable") @default(autoincrement())
|
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
|
day Int @db.SmallInt
|
||||||
teacher Int
|
odd Boolean
|
||||||
study_slot_timetable_slot_1Tostudy_slot study_slot? @relation("timetable_slot_1Tostudy_slot", fields: [slot_1], references: [id], map: "fk_timetable_study_slot_1")
|
slots study_slot[]
|
||||||
study_slot_timetable_slot_2Tostudy_slot study_slot? @relation("timetable_slot_2Tostudy_slot", fields: [slot_2], references: [id], map: "fk_timetable_study_slot_2")
|
teacher users @relation(fields: [teacherId], references: [id])
|
||||||
study_slot_timetable_slot_3Tostudy_slot study_slot? @relation("timetable_slot_3Tostudy_slot", fields: [slot_3], references: [id], map: "fk_timetable_study_slot_3")
|
teacherId Int
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model users {
|
model users {
|
||||||
|
|
|
||||||
82
src/components/StudyItemsList.astro
Normal file
82
src/components/StudyItemsList.astro
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
import { getStudyItems } from "../db";
|
||||||
|
|
||||||
|
const studyItems = await getStudyItems();
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<datalist id="siOptions">
|
||||||
|
{studyItems.map((si) => <option value={`${si.id} | ${si.title}`}>{si.title}</option>)}
|
||||||
|
</datalist>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h1>Предметы</h1>
|
||||||
|
<form class="row g-3" onsubmit="createNewStudyItem(this); return false;">
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="studyItemName" class="visually-hidden">Название предмета</label>
|
||||||
|
<input type="text" class="form-control" id="studyItemName" name="studyItemName" placeholder="Название предмета" />
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-primary mb-3">Создать</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="d-flex text-center gap-3 flex-wrap">
|
||||||
|
{
|
||||||
|
studyItems.map((item) => (
|
||||||
|
<div class="g-col-4 card p-2">
|
||||||
|
<p>{item.title}</p>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick={`deleteStudyItem(${item.id})`}>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{studyItems.length === 0 && <div class="g-col">Предметов нет</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
async function createNewStudyItem(form) {
|
||||||
|
try {
|
||||||
|
const fd = new FormData(form);
|
||||||
|
|
||||||
|
const resp = await fetch("/ttapi/createStudyItem", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error(json.reason);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteStudyItem(itemId) {
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("itemId", itemId);
|
||||||
|
|
||||||
|
const resp = await fetch("/ttapi/deleteStudyItem", {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error(json.reason);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
258
src/components/TimetableEditor.astro
Normal file
258
src/components/TimetableEditor.astro
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="container" data-user={editedUser?.login}>
|
||||||
|
{
|
||||||
|
editedUser === null && (
|
||||||
|
<form>
|
||||||
|
<label for="selectedUser" class="form-label">
|
||||||
|
Выбор пользователя
|
||||||
|
</label>
|
||||||
|
<input class="form-control mb-3" list="usersOptions" id="selectedUser" name="login" placeholder="Начните ввод, чтобы выбрать пользователя" />
|
||||||
|
<datalist id="usersOptions">
|
||||||
|
{allUsers.map((user) => (
|
||||||
|
<option value={user.login}>{user.login}</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
||||||
|
Выбрать
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
editedUser !== null && (
|
||||||
|
<>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Нечётная неделя</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<TimetableElement tt={oddTT[0]} position={0} />
|
||||||
|
<TimetableElement tt={oddTT[1]} position={1} />
|
||||||
|
<TimetableElement tt={oddTT[2]} position={2} />
|
||||||
|
<TimetableElement tt={oddTT[3]} position={3} />
|
||||||
|
<TimetableElement tt={oddTT[4]} position={4} />
|
||||||
|
<TimetableElement tt={oddTT[5]} position={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Чётная неделя</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<TimetableElement tt={evenTT[0]} position={0} />
|
||||||
|
<TimetableElement tt={evenTT[1]} position={1} />
|
||||||
|
<TimetableElement tt={evenTT[2]} position={2} />
|
||||||
|
<TimetableElement tt={evenTT[3]} position={3} />
|
||||||
|
<TimetableElement tt={evenTT[4]} position={4} />
|
||||||
|
<TimetableElement tt={evenTT[5]} position={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="fluindSaveButton">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
|
<>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" />
|
||||||
|
</>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<a id="fluindExitButton" href="/timetable">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
|
||||||
|
<>
|
||||||
|
<path d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
|
||||||
|
</>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function serializeTimetable() {
|
||||||
|
const result = [...document.querySelectorAll("[data-day]")].map((pe) => {
|
||||||
|
const data = [...pe!.querySelectorAll(".list-group > div")].map((e, idx) => {
|
||||||
|
const isWindow = (e.querySelector("[name=iw]")! as HTMLInputElement).checked;
|
||||||
|
const position = idx;
|
||||||
|
const day = parseInt((pe as HTMLInputElement).dataset.position!);
|
||||||
|
const studyItem = parseInt((e.querySelector("[name=si]")! as HTMLInputElement).value.split(" | ")[0]);
|
||||||
|
const slotPlace = (e.querySelector("[name=sp]")! as HTMLInputElement).value;
|
||||||
|
const slotGroup = (e.querySelector("[name=sg]")! as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (!isWindow && Number.isNaN(position) && Number.isNaN(studyItem) && slotPlace === "" && slotGroup === "") {
|
||||||
|
(e.querySelector("form")! as HTMLFormElement).reportValidity();
|
||||||
|
throw new Error("Некорректные данные");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
odd: (pe as HTMLElement).dataset.odd === "odd",
|
||||||
|
isWindow,
|
||||||
|
position,
|
||||||
|
day,
|
||||||
|
studyItem,
|
||||||
|
slotPlace,
|
||||||
|
slotGroup,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.reduce(
|
||||||
|
(acc, cur) => {
|
||||||
|
let newValue: {
|
||||||
|
isWindow: boolean;
|
||||||
|
position?: number;
|
||||||
|
day?: number;
|
||||||
|
studyItem?: number;
|
||||||
|
slotPlace?: string;
|
||||||
|
slotGroup?: string;
|
||||||
|
}[] = cur.map((e) => {
|
||||||
|
if (e.isWindow) {
|
||||||
|
return {
|
||||||
|
isWindow: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isWindow: false,
|
||||||
|
position: e.position,
|
||||||
|
day: e.day,
|
||||||
|
studyItem: e.studyItem,
|
||||||
|
slotPlace: e.slotPlace,
|
||||||
|
slotGroup: e.slotGroup,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (cur[0].odd) {
|
||||||
|
acc.odd.push(newValue);
|
||||||
|
} else {
|
||||||
|
acc.even.push(newValue);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
odd: [],
|
||||||
|
even: [],
|
||||||
|
} as {
|
||||||
|
odd: object[];
|
||||||
|
even: object[];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTimetable() {
|
||||||
|
try {
|
||||||
|
const serializedTimetable = serializeTimetable();
|
||||||
|
console.log(serializedTimetable);
|
||||||
|
|
||||||
|
const resp = await fetch("/ttapi/updateTimetable", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
login: document.querySelector("[data-user]")!.getAttribute("data-user"),
|
||||||
|
odd: serializedTimetable.odd,
|
||||||
|
even: serializedTimetable.even,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
if (json.ok) {
|
||||||
|
alert("Расписание сохранено");
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error(json.reason);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const forms = document.querySelectorAll(".needs-validation");
|
||||||
|
|
||||||
|
Array.from(forms).forEach((form) => {
|
||||||
|
form.addEventListener(
|
||||||
|
"submit",
|
||||||
|
(event) => {
|
||||||
|
if (!(form as HTMLFormElement).checkValidity()) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
form.classList.add("was-validated");
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("fluindSaveButton")?.addEventListener("click", () => {
|
||||||
|
saveTimetable();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#fluindSaveButton,
|
||||||
|
#fluindExitButton {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fluindSaveButton:hover,
|
||||||
|
#fluindExitButton:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#fluindSaveButton {
|
||||||
|
background-color: #007bff;
|
||||||
|
fill: white;
|
||||||
|
bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fluindExitButton {
|
||||||
|
background-color: #dc3545;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
src/components/TimetableElement.astro
Normal file
30
src/components/TimetableElement.astro
Normal file
|
|
@ -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);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="card w-100" data-day={tt.day} data-odd={tt.odd ? "odd" : "even"} data-position={position}>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{dayNames[tt.day]}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{
|
||||||
|
[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<TimetableSlotElement ss={slots.find((slot, idx) => slot.position == i) ?? null} position={i} day={tt.day} isOdd={tt.odd} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
33
src/components/TimetableElementViewer.astro
Normal file
33
src/components/TimetableElementViewer.astro
Normal file
|
|
@ -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 && (
|
||||||
|
<div class="card w-100" data-day={tt.day} data-odd={tt.odd ? "odd" : "even"} data-position={position}>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{dayNames[tt.day]}</h5>
|
||||||
|
</div>
|
||||||
|
<div class="list-group list-group-flush">
|
||||||
|
{[0, 1, 2, 3, 4, 5].map((i) => (
|
||||||
|
<TimetableSlotElementViewer ss={slots.find((slot, idx) => slot.position == i) ?? null} position={i} day={tt.day} isOdd={tt.odd} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
120
src/components/TimetableSlotElement.astro
Normal file
120
src/components/TimetableSlotElement.astro
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
ss === null && (
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id={`isWindow-${day}-${position}`}
|
||||||
|
data-target={`dh-${day}-${position}-${isOdd ? "o" : "e"}`}
|
||||||
|
name="iw"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ss !== null && (
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
id={`isWindow-${day}-${position}`}
|
||||||
|
data-target={`dh-${day}-${position}-${isOdd ? "o" : "e"}`}
|
||||||
|
name="iw"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<label class="form-check-label" for={`isWindow-${day}-${position}`}> Окно</label>
|
||||||
|
<div>{positionToTime[position]}</div>
|
||||||
|
<form
|
||||||
|
id={`dh-${day}-${position}-${isOdd ? "o" : "e"}`}
|
||||||
|
class:list={[
|
||||||
|
{
|
||||||
|
"d-none": ss === null,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<label for={`slotSI-${day}-${position}`} class="form-label"> Предмет</label>
|
||||||
|
<input
|
||||||
|
class="form-control mb-3"
|
||||||
|
list="siOptions"
|
||||||
|
id={`slotSI-${day}-${position}`}
|
||||||
|
placeholder="Начните ввод"
|
||||||
|
name="si"
|
||||||
|
value={studyItem !== null ? `${(studyItem as study_item).id} | ${(studyItem as study_item).title}` : ""}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for={`slotPlace-${day}-${position}`} class="form-label"> Место проведения</label>
|
||||||
|
<input class="form-control mb-3" id={`slotPlace-${day}-${position}`} name="sp" value={ss !== null ? (ss as study_slot).where : ""} required />
|
||||||
|
|
||||||
|
<label for={`slotGroup-${day}-${position}`} class="form-label"> Группа</label>
|
||||||
|
<input
|
||||||
|
class="form-control mb-3"
|
||||||
|
id={`slotGroup-${day}-${position}`}
|
||||||
|
name="sg"
|
||||||
|
value={ss !== null ? (ss as study_slot).studentsGroup : ""}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
ss !== null && (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<div>{studyItem!.title}</div>
|
||||||
|
<div>{positionToTime[position]}</div>
|
||||||
|
<div>Где: {ss.where}</div>
|
||||||
|
<div>Группа(ы): {ss.studentsGroup}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll("[data-target]").forEach((e) => {
|
||||||
|
e.addEventListener("change", () => {
|
||||||
|
const targetId = e.getAttribute("data-target");
|
||||||
|
if (targetId === null) {
|
||||||
|
console.error("targetId is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = document.getElementById(targetId) as HTMLInputElement;
|
||||||
|
if ((e as HTMLInputElement).checked) {
|
||||||
|
target.classList.add("d-none");
|
||||||
|
target.querySelectorAll("input").forEach((e) => {
|
||||||
|
e.required = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
target.classList.remove("d-none");
|
||||||
|
target.querySelectorAll("input").forEach((e) => {
|
||||||
|
e.required = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
44
src/components/TimetableSlotElementViewer.astro
Normal file
44
src/components/TimetableSlotElementViewer.astro
Normal file
|
|
@ -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 && (
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div>
|
||||||
|
<div>{studyItem!.title}</div>
|
||||||
|
<div>{positionToTime[position]}</div>
|
||||||
|
<div>Где: {ss.where}</div>
|
||||||
|
<div>Группа(ы): {ss.studentsGroup}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
ss === null && (
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div>
|
||||||
|
<div>Свободно</div>
|
||||||
|
<div>{positionToTime[position]}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)*/
|
||||||
|
}
|
||||||
51
src/components/TimetableViewer.astro
Normal file
51
src/components/TimetableViewer.astro
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row mt-5 mb-5">
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Нечётная неделя</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<TimetableElementViewer tt={oddTT[0]} position={0} />
|
||||||
|
<TimetableElementViewer tt={oddTT[1]} position={1} />
|
||||||
|
<TimetableElementViewer tt={oddTT[2]} position={2} />
|
||||||
|
<TimetableElementViewer tt={oddTT[3]} position={3} />
|
||||||
|
<TimetableElementViewer tt={oddTT[4]} position={4} />
|
||||||
|
<TimetableElementViewer tt={oddTT[5]} position={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h5>Чётная неделя</h5>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<TimetableElementViewer tt={evenTT[0]} position={0} />
|
||||||
|
<TimetableElementViewer tt={evenTT[1]} position={1} />
|
||||||
|
<TimetableElementViewer tt={evenTT[2]} position={2} />
|
||||||
|
<TimetableElementViewer tt={evenTT[3]} position={3} />
|
||||||
|
<TimetableElementViewer tt={evenTT[4]} position={4} />
|
||||||
|
<TimetableElementViewer tt={evenTT[5]} position={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
153
src/db.ts
153
src/db.ts
|
|
@ -218,3 +218,156 @@ export async function searchUsers(params: { login?: string; isAdmin?: boolean; f
|
||||||
});
|
});
|
||||||
return users;
|
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), []),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
32
src/pages/timetable.astro
Normal file
32
src/pages/timetable.astro
Normal file
|
|
@ -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))!;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="Пользователи">
|
||||||
|
<main>
|
||||||
|
<Navbar is_user_admin={user.is_admin} />
|
||||||
|
{user.is_admin && <StudyItemsList />}
|
||||||
|
{user.is_admin && <TimetableEditor />}
|
||||||
|
{!user.is_admin && <TimetableViewer user={user} />}
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
39
src/pages/ttapi/createStudyItem.ts
Normal file
39
src/pages/ttapi/createStudyItem.ts
Normal file
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
39
src/pages/ttapi/deleteStudyItem.ts
Normal file
39
src/pages/ttapi/deleteStudyItem.ts
Normal file
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
44
src/pages/ttapi/updateTimetable.ts
Normal file
44
src/pages/ttapi/updateTimetable.ts
Normal file
|
|
@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue