Добавил работу с расписанием
This commit is contained in:
parent
e9b8b44d1b
commit
35a20acce6
13 changed files with 939 additions and 25 deletions
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;
|
||||
}
|
||||
|
||||
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