Доделываю перед сдачей
This commit is contained in:
parent
1175ccff74
commit
0ebbe91f4f
15 changed files with 906 additions and 15 deletions
|
|
@ -50,14 +50,15 @@ model study_item {
|
|||
}
|
||||
|
||||
model study_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?
|
||||
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?
|
||||
user_lesson_attended user_lesson_attended[]
|
||||
}
|
||||
|
||||
model timetable {
|
||||
|
|
@ -78,11 +79,13 @@ model users {
|
|||
is_moderator Boolean @default(false)
|
||||
lore String? @db.Text()
|
||||
|
||||
timetable timetable[]
|
||||
user_session user_session[]
|
||||
chat_message chat_message[]
|
||||
user_in_chat user_in_chat[]
|
||||
attended event_attendance[]
|
||||
timetable timetable[]
|
||||
user_session user_session[]
|
||||
chat_message chat_message[]
|
||||
user_in_chat user_in_chat[]
|
||||
attended event_attendance[]
|
||||
user_lesson_attended user_lesson_attended[]
|
||||
documents documents[]
|
||||
}
|
||||
|
||||
model user_session {
|
||||
|
|
@ -122,3 +125,34 @@ model user_in_chat {
|
|||
chat chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
||||
user users @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model user_lesson_attended {
|
||||
user users @relation(fields: [userId], references: [id])
|
||||
slot study_slot @relation(fields: [slotId], references: [id])
|
||||
day DateTime @default(now())
|
||||
|
||||
userId Int
|
||||
slotId Int
|
||||
|
||||
@@id([userId, slotId, day])
|
||||
}
|
||||
|
||||
enum DocumentStatus {
|
||||
STARTED
|
||||
IN_PROGRESS
|
||||
FINISHED
|
||||
}
|
||||
|
||||
model documents {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
status DocumentStatus @default(STARTED)
|
||||
comment String @default("")
|
||||
filename String?
|
||||
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
user users @relation(fields: [usersId], references: [id])
|
||||
usersId Int
|
||||
}
|
||||
|
|
|
|||
22
public/assets/js/xlsx.full.min.js
vendored
Normal file
22
public/assets/js/xlsx.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
public/documents/.gitignore
vendored
Normal file
3
public/documents/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
*
|
||||
!.gitkeep
|
||||
!.gitignore
|
||||
0
public/documents/.gitkeep
Normal file
0
public/documents/.gitkeep
Normal file
|
|
@ -25,6 +25,10 @@ const items = [
|
|||
href: "/chats",
|
||||
title: "Чаты",
|
||||
},
|
||||
{
|
||||
href: "/docs",
|
||||
title: "Документы",
|
||||
},
|
||||
];
|
||||
|
||||
const itemsRight = [
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const studyItems = await getStudyItems();
|
|||
</datalist>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Предметы</h1>
|
||||
<h1 class="mt-5 mb-5">Предметы</h1>
|
||||
<form class="row g-3" onsubmit="createNewStudyItem(this); return false;">
|
||||
<div class="col-auto">
|
||||
<label for="studyItemName" class="visually-hidden">Название предмета</label>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ if (editedUser !== null) {
|
|||
---
|
||||
|
||||
<div class="container mb-5" data-user={editedUser?.login}>
|
||||
<a class="btn btn-sm btn-primary" href="/joint-timetable">Перейти в сводную таблицу расписаний</a>
|
||||
<hr/>
|
||||
{
|
||||
editedUser === null && (
|
||||
<form>
|
||||
|
|
|
|||
119
src/db.ts
119
src/db.ts
|
|
@ -1,8 +1,9 @@
|
|||
import { PrismaClient, chat, chat_message, user_in_chat, users, EventAttendanceType } from "@prisma/client";
|
||||
import { PrismaClient, chat, chat_message, user_in_chat, users, EventAttendanceType, DocumentStatus } from "@prisma/client";
|
||||
import { blake3 } from "@noble/hashes/blake3";
|
||||
import { bytesToHex as toHex } from "@noble/hashes/utils";
|
||||
|
||||
export type EventAttendanceTypeV = EventAttendanceType;
|
||||
export type DocumentStatusV = DocumentStatus;
|
||||
|
||||
const client = new PrismaClient();
|
||||
|
||||
|
|
@ -581,6 +582,7 @@ export async function createEvent(title: string, description: string, when: Date
|
|||
|
||||
return article !== null;
|
||||
}
|
||||
|
||||
export async function attendEvent(eventId: number, userId: number, attendanceType: string) {
|
||||
if (attendanceType === "UNDO") {
|
||||
const article = await client.event_attendance.delete({
|
||||
|
|
@ -620,3 +622,118 @@ export async function attendEvent(eventId: number, userId: number, attendanceTyp
|
|||
return article !== null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeachersAndTheirTimetables() {
|
||||
const tats = await client.users.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
login: true,
|
||||
timetable: {
|
||||
select: {
|
||||
id: true,
|
||||
odd: true,
|
||||
day: true,
|
||||
slots: {
|
||||
select: {
|
||||
id: true,
|
||||
position: true,
|
||||
study_item: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
where: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return tats;
|
||||
}
|
||||
|
||||
export async function getTeachersDocuments(teacherId: number | undefined) {
|
||||
const documents = await client.documents.findMany({
|
||||
where: {
|
||||
usersId: teacherId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
status: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
user: true,
|
||||
comment: true,
|
||||
filename: true,
|
||||
},
|
||||
orderBy: {
|
||||
updated_at: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
export async function createNewDocument(title: string, userId: number) {
|
||||
const document = await client.documents.create({
|
||||
data: {
|
||||
title: title,
|
||||
usersId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
export async function deleteDocument(documentId: string) {
|
||||
const document = await client.documents.delete({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
});
|
||||
|
||||
return document !== null;
|
||||
}
|
||||
|
||||
export async function updateDocumentComment(documentId: string, comment: string) {
|
||||
const document = await client.documents.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
comment: comment,
|
||||
},
|
||||
});
|
||||
|
||||
return document !== null;
|
||||
}
|
||||
|
||||
export async function updateDocumentFilename(documentId: string, filename: string) {
|
||||
const document = await client.documents.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
filename: filename,
|
||||
},
|
||||
});
|
||||
|
||||
return document !== null;
|
||||
}
|
||||
|
||||
export async function updateDocumentStatus(documentId: string, documentStatus: string) {
|
||||
const document = await client.documents.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
},
|
||||
data: {
|
||||
status: documentStatus as DocumentStatus,
|
||||
},
|
||||
});
|
||||
|
||||
return document !== null;
|
||||
}
|
||||
|
|
|
|||
320
src/pages/docs.astro
Normal file
320
src/pages/docs.astro
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { getSessionUser, getTeachersDocuments, getUserSession } from "../db";
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
|
||||
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 formatDate = (dt: Date) => {
|
||||
return (
|
||||
("0" + dt.getDay()).substr(-2) +
|
||||
"." +
|
||||
("0" + (0 + dt.getMonth())).substr(-2) +
|
||||
"." +
|
||||
dt.getFullYear() +
|
||||
" в " +
|
||||
("0" + dt.getHours()).substr(-2) +
|
||||
":" +
|
||||
("0" + dt.getMinutes()).substr(-2)
|
||||
);
|
||||
};
|
||||
|
||||
const sessId = Astro.cookies.get("session").value!;
|
||||
const user = (await getSessionUser(sessId))!;
|
||||
|
||||
const docs = await getTeachersDocuments(user.is_admin || user.is_moderator ? undefined : user.id);
|
||||
---
|
||||
|
||||
<Layout title="Документы">
|
||||
<datalist id="statusOpts">
|
||||
<option value="Начат">STARTED</option>
|
||||
<option value="В работе">IN_PROGRESS</option>
|
||||
<option value="Закончен">FINISHED</option>
|
||||
</datalist>
|
||||
<main>
|
||||
<Navbar is_user_admin={user.is_admin} user={user} />
|
||||
<div class="container-fluid mt-5">
|
||||
<h1 class="mb-5">Документы</h1>
|
||||
<form id="newDocForm">
|
||||
<div class="mb-3">
|
||||
<label for="newDocName" class="col-sm-2 col-form-label">Название нового документа</label>
|
||||
<div class="d-flex flex-row gap-3">
|
||||
<input type="text" class="form-control" name="newDocName" />
|
||||
<button class="btn btn-primary w-25" type="submit">Создать новый документ</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<hr />
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{user.is_admin || user.is_moderator ? <th scope="col">ФИО</th> : null}
|
||||
<th scope="col">Название документа</th>
|
||||
<th scope="col">Статус</th>
|
||||
<th scope="col">Комментарий</th>
|
||||
<th scope="col">Файл</th>
|
||||
<th scope="col">Действия</th>
|
||||
<th scope="col">Создано/изменено</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
docs.map((doc) => (
|
||||
<tr>
|
||||
{user.is_admin || user.is_moderator ? <th scope="col">{doc.user.fullName || doc.user.login}</th> : null}
|
||||
<td>{doc.title}</td>
|
||||
<td class="statusCell">
|
||||
<div class="btn-group" role="group">
|
||||
{doc.status === "STARTED" ? (
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name={`docStatus-${doc.id}`}
|
||||
id={`docStarted-${doc.id}`}
|
||||
value="STARTED"
|
||||
checked
|
||||
/>
|
||||
) : (
|
||||
<input type="radio" class="btn-check" name={`docStatus-${doc.id}`} id={`docStarted-${doc.id}`} value="STARTED" />
|
||||
)}
|
||||
<label class="btn btn-outline-primary btn-sm" for={`docStarted-${doc.id}`}>
|
||||
Начат
|
||||
</label>
|
||||
|
||||
{doc.status === "IN_PROGRESS" ? (
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name={`docStatus-${doc.id}`}
|
||||
id={`docInProgress-${doc.id}`}
|
||||
value="IN_PROGRESS"
|
||||
checked
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name={`docStatus-${doc.id}`}
|
||||
id={`docInProgress-${doc.id}`}
|
||||
value="IN_PROGRESS"
|
||||
/>
|
||||
)}
|
||||
<label class="btn btn-outline-primary btn-sm" for={`docInProgress-${doc.id}`}>
|
||||
В прогрессе
|
||||
</label>
|
||||
|
||||
{doc.status === "FINISHED" ? (
|
||||
<input
|
||||
type="radio"
|
||||
class="btn-check"
|
||||
name={`docStatus-${doc.id}`}
|
||||
id={`docFinished-${doc.id}`}
|
||||
value="FINISHED"
|
||||
checked
|
||||
/>
|
||||
) : (
|
||||
<input type="radio" class="btn-check" name={`docStatus-${doc.id}`} id={`docFinished-${doc.id}`} value="FINISHED" />
|
||||
)}
|
||||
<label class="btn btn-outline-primary btn-sm" for={`docFinished-${doc.id}`}>
|
||||
Закончен
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td class="commentCell">{doc.comment}</td>
|
||||
<td>
|
||||
{doc.filename === null ? (
|
||||
<form class="uploadDocForm" onsubmit={"return false"}>
|
||||
<input type="file" name="docFile" id="docFile" style="width: 5.1rem;" />
|
||||
<input type="hidden" name="docId" value={doc.id} />
|
||||
<button type="submit">Загрузить</button>
|
||||
</form>
|
||||
) : (
|
||||
<a href={`/documents/${doc.filename}`} target="_blank">
|
||||
Скачать
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-primary btn-ucom" data-docId={doc.id}>
|
||||
Изменить комментарий
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger btn-ddoc" data-docId={doc.id}>
|
||||
Удалить
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
{formatDate(doc.created_at)} / {formatDate(doc.updated_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
async function changeDocStatus(docId: string, docStatus: string) {
|
||||
try {
|
||||
const resp = await fetch("/docsapi/updateStatus", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
docId,
|
||||
docStatus,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error(json.reason);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteDoc(docId: string) {
|
||||
try {
|
||||
const resp = await fetch("/docsapi/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
docId,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error(json.reason);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateDocComment(docId: string, comment: string) {
|
||||
try {
|
||||
const resp = await fetch("/docsapi/updateComment", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
docId,
|
||||
comment,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error(json.reason);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof Error) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadDoc(fd: FormData) {
|
||||
try {
|
||||
const resp = await fetch("/docsapi/uploadFile", {
|
||||
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);
|
||||
if (e instanceof Error) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("newDocForm")!.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const resp = await fetch("/docsapi/create", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert("Произошла ошибка при создании документа");
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("input.btn-check[type=radio]").forEach((e) => {
|
||||
e.addEventListener("change", function () {
|
||||
const elemName = (e as HTMLInputElement).name;
|
||||
const dId = elemName.substr(10);
|
||||
const docState = (e as HTMLInputElement).value;
|
||||
changeDocStatus(dId, docState);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("button.btn-ucom").forEach((e) => {
|
||||
e.addEventListener("click", function () {
|
||||
const docid = (e as HTMLInputElement).dataset["docid"]!;
|
||||
|
||||
const newComm = prompt("Введите новый комментарий");
|
||||
if (newComm === null) return;
|
||||
|
||||
updateDocComment(docid, newComm);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("button.btn-ddoc").forEach((e) => {
|
||||
e.addEventListener("click", function () {
|
||||
const docid = (e as HTMLInputElement).dataset["docid"]!;
|
||||
deleteDoc(docid);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".uploadDocForm").forEach((e) => {
|
||||
e.addEventListener("submit", function () {
|
||||
const fd = new FormData(e as HTMLFormElement);
|
||||
uploadDoc(fd);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
31
src/pages/docsapi/create.ts
Normal file
31
src/pages/docsapi/create.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { APIContext } from "astro";
|
||||
import { createNewDocument, getSessionUser } from "../../db";
|
||||
|
||||
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))!;
|
||||
|
||||
const fd = await request.formData();
|
||||
const newDocName = fd.get("newDocName");
|
||||
if (!newDocName) {
|
||||
throw new Error("Неправильный формат запроса");
|
||||
}
|
||||
|
||||
await createNewDocument(newDocName.toString(), user.id);
|
||||
} catch (e: any) {
|
||||
response.ok = false;
|
||||
if (e instanceof Error) {
|
||||
response.reason = e.message;
|
||||
} else {
|
||||
response.reason = e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
body: JSON.stringify(response),
|
||||
};
|
||||
}
|
||||
39
src/pages/docsapi/delete.ts
Normal file
39
src/pages/docsapi/delete.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import type { APIContext } from "astro";
|
||||
import { getSessionUser, deleteDocument } 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))!;
|
||||
|
||||
const json = await request.json();
|
||||
const docId = json.docId as string;
|
||||
if (!docId) {
|
||||
throw new Error("Предоставлены некорректные данные");
|
||||
}
|
||||
|
||||
const ok = await deleteDocument(docId);
|
||||
if (!ok) {
|
||||
throw new Error("Неизвестная ошибка");
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
40
src/pages/docsapi/updateComment.ts
Normal file
40
src/pages/docsapi/updateComment.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { APIContext } from "astro";
|
||||
import { getSessionUser, updateDocumentComment } 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))!;
|
||||
|
||||
const json = await request.json();
|
||||
const docId = json.docId as string;
|
||||
const comment = json.comment as string;
|
||||
if (!docId || comment === null) {
|
||||
throw new Error("Предоставлены некорректные данные");
|
||||
}
|
||||
|
||||
const ok = await updateDocumentComment(docId, comment);
|
||||
if (!ok) {
|
||||
throw new Error("Неизвестная ошибка");
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
40
src/pages/docsapi/updateStatus.ts
Normal file
40
src/pages/docsapi/updateStatus.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { APIContext } from "astro";
|
||||
import { getSessionUser, updateDocumentStatus } 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))!;
|
||||
|
||||
const json = await request.json();
|
||||
const docId = json.docId as string;
|
||||
const docStatus = json.docStatus as string;
|
||||
if (!docId || !docStatus) {
|
||||
throw new Error("Предоставлены некорректные данные");
|
||||
}
|
||||
|
||||
const ok = await updateDocumentStatus(docId, docStatus);
|
||||
if (!ok) {
|
||||
throw new Error("Неизвестная ошибка");
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
47
src/pages/docsapi/uploadFile.ts
Normal file
47
src/pages/docsapi/uploadFile.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { APIContext } from "astro";
|
||||
import { updateDocumentFilename } from "../../db";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join as joinPath } from "path";
|
||||
import { cwd } from "process";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
function randomHash() {
|
||||
return (Math.random() * 1e30).toString(16);
|
||||
}
|
||||
|
||||
export async function post({ request }: APIContext) {
|
||||
const response: { ok: boolean; reason?: string } = {
|
||||
ok: true,
|
||||
};
|
||||
|
||||
try {
|
||||
const fd = await request.formData();
|
||||
const docId = fd.get("docId") as string;
|
||||
const blob = fd.get("docFile") as Blob;
|
||||
|
||||
if (!docId || !blob || blob.size <= 0 || blob.name === undefined) {
|
||||
throw new Error("Предоставлены некорректные данные");
|
||||
}
|
||||
|
||||
console.log(docId, blob, blob.name);
|
||||
|
||||
const fName = `${randomHash()}_${blob.name}`;
|
||||
const buf = Buffer.from(await blob.arrayBuffer());
|
||||
|
||||
await updateDocumentFilename(docId, fName);
|
||||
await writeFile(joinPath(cwd(), "public", "documents", fName), buf);
|
||||
} 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),
|
||||
};
|
||||
}
|
||||
192
src/pages/joint-timetable.astro
Normal file
192
src/pages/joint-timetable.astro
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import { getSessionUser, getTeachersAndTheirTimetables } from "../db";
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
|
||||
const sessId = Astro.cookies.get("session").value!;
|
||||
const user = await getSessionUser(sessId);
|
||||
|
||||
if (user === null || (!user.is_admin && !user.is_moderator)) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const days = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"];
|
||||
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"];
|
||||
|
||||
const tats = await getTeachersAndTheirTimetables();
|
||||
|
||||
const renderDay = function (subj: any | undefined, time: string, tIdx: number) {
|
||||
if (subj === undefined) {
|
||||
return `${time} - Нет занятия`;
|
||||
}
|
||||
|
||||
return `${time} - ${subj.where}`;
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Сводная таблица расписаний">
|
||||
<main>
|
||||
<Navbar is_user_admin={user.is_admin} user={user} />
|
||||
<div class="container-fluid mt-5">
|
||||
<h1 class="mb-5">Сводная таблица расписаний</h1>
|
||||
<button class="btn btn-sm btn-primary" id="cellsMuter">Затушить ячейки без занятий</button>
|
||||
<button class="btn btn-sm btn-primary" onclick="downloadTables()">Скачать таблицы</button>
|
||||
<hr />
|
||||
<h4>Нечётная неделя</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ФИО</th>
|
||||
{days.map((day) => <th scope="col">{day}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
tats.map((teacher) =>
|
||||
positionToTime.map((tp, tIdx) => (
|
||||
<tr>
|
||||
<td>{teacher.fullName || teacher.login}</td>
|
||||
{days.map((_, dIdx) => (
|
||||
<td>
|
||||
{renderDay(
|
||||
teacher.timetable.find((ttel) => ttel.day == dIdx && ttel.odd)?.slots.find((slt) => slt.position == tIdx),
|
||||
tp,
|
||||
dIdx
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h4>Чётная неделя</h4>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ФИО</th>
|
||||
{days.map((day) => <th scope="col">{day}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
tats.map((teacher) =>
|
||||
positionToTime.map((tp, tIdx) => (
|
||||
<tr>
|
||||
<td>{teacher.fullName || teacher.login}</td>
|
||||
{days.map((_, dIdx) => (
|
||||
<td>
|
||||
{renderDay(
|
||||
teacher.timetable.find((ttel) => ttel.day == dIdx && !ttel.odd)?.slots.find((slt) => slt.position == tIdx),
|
||||
tp,
|
||||
dIdx
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<script src="/assets/js/xlsx.full.min.js" is:inline></script>
|
||||
|
||||
<script is:inline>
|
||||
function downloadTables() {
|
||||
const table_elts = document.querySelectorAll("table");
|
||||
const worksheetOdd = XLSX.utils.table_to_sheet(table_elts[0]);
|
||||
const worksheetEven = XLSX.utils.table_to_sheet(table_elts[1]);
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
|
||||
XLSX.utils.book_append_sheet(workbook, worksheetOdd, "Нечётные недели");
|
||||
XLSX.utils.book_append_sheet(workbook, worksheetEven, "Чётные недели");
|
||||
|
||||
XLSX.write(workbook, { bookType: "xlsx", bookSST: true, type: "base64" });
|
||||
XLSX.writeFile(workbook, "Сводная таблица расписаний.xlsx");
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function muteCells() {
|
||||
const tables = [...document.querySelectorAll("table")];
|
||||
for (const table of tables) {
|
||||
const rows = table.querySelectorAll("tr");
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.querySelectorAll("td");
|
||||
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const cell = cells[j];
|
||||
|
||||
if (cell.innerText.includes("Нет занятия")) {
|
||||
cell.classList.toggle("muted");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupTable(table: HTMLTableElement) {
|
||||
const rows = table.querySelectorAll("tr");
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const cells = row.querySelectorAll("td");
|
||||
|
||||
for (let j = 0; j < cells.length; j++) {
|
||||
const cell = cells[j];
|
||||
|
||||
if (j == 0) {
|
||||
let sameTextCells = [cell];
|
||||
|
||||
for (let k = i + 1; k < rows.length; k++) {
|
||||
const nextRow = rows[k];
|
||||
const nextCell = nextRow.querySelectorAll("td")[j];
|
||||
|
||||
if (nextCell.innerText == cell.innerText) {
|
||||
sameTextCells.push(nextCell);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (sameTextCells.length > 1) {
|
||||
sameTextCells[0].rowSpan = sameTextCells.length;
|
||||
sameTextCells[0].style.verticalAlign = "middle";
|
||||
|
||||
for (let k = 1; k < sameTextCells.length; k++) {
|
||||
sameTextCells[k].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelector("#cellsMuter")!.addEventListener("click", () => {
|
||||
muteCells();
|
||||
});
|
||||
|
||||
const tables = document.querySelectorAll("table");
|
||||
tables.forEach((table) => cleanupTable(table));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #aaa !important;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue