sposchedule
403 строки · 11.8 Кб
1<script setup lang="ts">
2import { ref } from 'vue';
3import DataTable from 'primevue/datatable';
4import Column from 'primevue/column';
5import Select from 'primevue/select';
6import InputText from 'primevue/inputtext';
7import Button from 'primevue/button';
8import { useDateFormat } from '@vueuse/core';
9import { useToast } from 'primevue/usetoast';
10import Chip from 'primevue/chip';
11import MultiSelect from 'primevue/multiselect';
12import { FilterMatchMode } from '@primevue/core/api';
13import Textarea from 'primevue/textarea';
14import {
15useDestroyGroup,
16useGroupsQuery,
17useStoreGroup,
18useUpdateGroup,
19} from '../../queries/groups';
20import { useSemestersQuery } from '@/queries/semesters';
21import { useConfirm } from 'primevue/useconfirm';
22import { useBuildingsQuery } from '@/queries/buildings';
23
24const toast = useToast();
25const { data: groups } = useGroupsQuery();
26
27const newGroupName = ref('');
28const newGroupError = ref(false);
29
30const editingRows = ref([]);
31const selectedGroups = ref([]);
32const courses = [
33{
34label: 1,
35value: 1,
36},
37{
38label: 2,
39value: 2,
40},
41{
42label: 3,
43value: 3,
44},
45{
46label: 4,
47value: 4,
48},
49];
50
51const { mutateAsync: updateGroup, isPending: isUpdated } = useUpdateGroup();
52
53const onRowEditSave = async event => {
54let { newData } = event;
55
56try {
57await updateGroup({
58id: newData.id,
59body: newData,
60});
61} catch (e) {
62toast.add({
63severity: 'error',
64summary: 'Ошибка',
65detail: e?.response.data.message,
66life: 3000,
67closable: true,
68});
69return;
70}
71};
72
73const regexGroup = /^[a-zA-Zа-яА-Я]{2,4}-[1-4]\d{1,4}$/;
74const { mutateAsync: storeSubject, isPending: isStored } = useStoreGroup();
75
76const addGroup = async () => {
77if (!regexGroup.test(newGroupName.value)) {
78toast.add({
79severity: 'error',
80summary: 'Ошибка',
81detail: 'Неверный формат названия Группы. Пример: ИС-401',
82life: 3000,
83closable: true,
84});
85newGroupError.value = true;
86return;
87}
88
89try {
90await storeSubject({
91specialization: newGroupName.value.split('-')[0],
92course: newGroupName.value.split('-')[1][0],
93index: newGroupName.value.split('-')[1].slice(1),
94semesters: selectedSemesters.value,
95buildings: selectedBuildings.value,
96});
97} catch (e) {
98toast.add({
99severity: 'error',
100summary: 'Ошибка',
101detail: e?.response.data.message,
102life: 3000,
103closable: true,
104});
105newGroupName.value = '';
106}
107
108newGroupError.value = false;
109newGroupName.value = '';
110selectedSemesters.value = [];
111selectedBuildings.value = [];
112};
113
114const confirm = useConfirm();
115
116const { mutateAsync: destroyGroup, isPending: isDestroyed } =
117useDestroyGroup();
118
119const confirmDelete = () => {
120confirm.require({
121message: 'Удаление групп может сломать расписание',
122header: 'Вы уверены?',
123icon: 'pi pi-info-circle',
124rejectLabel: 'Отмена',
125rejectProps: {
126label: 'Отмена',
127severity: 'secondary',
128outlined: true,
129},
130acceptProps: {
131label: 'Удалить',
132severity: 'danger',
133},
134accept: async () => {
135await deleteGroups();
136},
137reject: () => {},
138});
139};
140
141const deleteGroups = async () => {
142if (selectedGroups.value.length === 0) return;
143
144for (let i = 0; i < selectedGroups.value.length; i++) {
145try {
146await destroyGroup(selectedGroups.value[i].id);
147} catch (e) {
148toast.add({
149severity: 'error',
150summary: 'Ошибка',
151detail: e?.response.data.message,
152life: 3000,
153closable: true,
154});
155return;
156}
157}
158selectedGroups.value = [];
159};
160
161const { data: semesters } = useSemestersQuery();
162
163const selectedSemesters = ref([]);
164
165const filters = ref({
166global: { value: null, matchMode: FilterMatchMode.CONTAINS },
167name: { value: null, matchMode: FilterMatchMode.STARTS_WITH },
168course: { value: null, matchMode: FilterMatchMode.STARTS_WITH },
169});
170
171const importGroupsState = ref();
172const importingGroups = ref();
173
174const parseAndSendGroups = async () => {
175// Разделяем введенные группы на массив строк, убирая пустые строки и пробелы
176const groups = importingGroups.value
177.split('\n')
178.map(group => group.trim())
179.filter(group => group);
180
181// Проходим по каждой группе и отправляем её на сервер
182for (const group of groups) {
183if (!regexGroup.test(group)) {
184toast.add({
185severity: 'error',
186summary: 'Ошибка',
187detail: `Неверный формат названия группы: ${group}. Пример: ИС-401`,
188life: 3000,
189closable: true,
190});
191continue; // Пропускаем группу с неверным форматом
192}
193
194try {
195// Отправляем данные на сервер
196await storeSubject({
197specialization: group.split('-')[0],
198course: group.split('-')[1][0],
199index: group.split('-')[1].slice(1),
200semesters: selectedSemesters.value,
201buildings: selectedBuildings.value,
202});
203
204// Успешное добавление группы
205toast.add({
206severity: 'success',
207summary: 'Успех',
208detail: `Группа ${group} успешно добавлена`,
209life: 3000,
210closable: true,
211});
212} catch (e) {
213// Обработка ошибки при отправке
214toast.add({
215severity: 'error',
216summary: 'Ошибка',
217detail:
218e?.response?.data?.message ||
219`Ошибка при добавлении группы ${group}`,
220life: 3000,
221closable: true,
222});
223}
224}
225
226// Очистка поля после завершения отправки
227importingGroups.value = '';
228selectedSemesters.value = [];
229selectedBuildings.value = [];
230};
231
232const { data: buildings } = useBuildingsQuery();
233const selectedBuildings = ref();
234</script>
235
236<template>
237<div class="flex flex-col gap-4">
238<div class="flex flex-wrap justify-between items-baseline">
239<h1 class="text-2xl">Группы</h1>
240</div>
241<div class="">
242<form
243class="flex flex-wrap items-center gap-4 p-4 rounded-lg bg-surface-100 dark:bg-surface-800"
244>
245<InputText
246v-model="newGroupName"
247:invalid="newGroupError"
248placeholder="Пример: ИС-401"
249class="w-full md:w-56"
250/>
251<MultiSelect
252v-model="selectedSemesters"
253display="chip"
254:options="semesters"
255option-label="name"
256filter
257:max-selected-labels="3"
258placeholder="Выбрать семестры"
259class="w-full md:w-60"
260/>
261<MultiSelect
262v-model="selectedBuildings"
263filter
264auto-filter-focus
265option-label="name"
266:options="buildings"
267placeholder="Корпус"
268class="w-full md:w-36"
269/>
270
271<Button
272type="submit"
273:disabled="!newGroupName"
274@click.prevent="addGroup"
275>
276Добавить группу
277</Button>
278<Button
279icon="pi pi-file-import"
280outlined
281type="submit"
282label="Импорт"
283@click.prevent="importGroupsState = !importGroupsState"
284/>
285<div v-if="importGroupsState" class="flex flex-col gap-2">
286<Textarea
287v-model="importingGroups"
288placeholder="Введите в столбик название групп"
289rows="5"
290cols="30"
291/>
292<Button type="submit" @click.prevent="parseAndSendGroups">
293Импортировать
294</Button>
295</div>
296</form>
297</div>
298<div class="">
299<DataTable
300v-model:filters="filters"
301v-model:selection="selectedGroups"
302v-model:editing-rows="editingRows"
303paginator
304:rows="10"
305:global-filter-fields="['name', 'course']"
306:loading="isUpdated || isDestroyed || isStored"
307:value="groups"
308edit-mode="row"
309data-key="id"
310:pt="{
311table: { style: 'min-width: 50rem' },
312}"
313@row-edit-save="onRowEditSave"
314>
315<template #header>
316<div class="flex flex-wrap items-center gap-2 justify-between">
317<Button
318severity="danger"
319:disabled="!selectedGroups.length || !groups.length"
320type="button"
321icon="pi pi-trash"
322label="Удалить"
323outlined
324@click="confirmDelete"
325/>
326<InputText v-model="filters['global'].value" placeholder="Поиск" />
327</div>
328</template>
329<Column selection-mode="multiple" header-style="width: 3rem" />
330<Column field="name" header="Название группы" />
331<Column field="buildings" header="Корпус">
332<template #body="slotProps">
333<div class="flex gap-2 flex-wrap">
334<Chip
335v-for="building in slotProps.data.buildings"
336:label="building.name"
337/>
338</div>
339</template>
340<template #editor="{ data, field }">
341<MultiSelect
342v-model="data.buildings"
343data-key="name"
344:options="buildings"
345display="chip"
346option-label="name"
347filter
348placeholder="Выберите корпуса"
349class=""
350/>
351</template>
352</Column>
353<Column field="specialization" header="Специальность">
354<template #editor="{ data, field }">
355<InputText v-model="data[field]" fluid />
356</template>
357</Column>
358<Column field="course" header="Курс">
359<template #editor="{ data, field }">
360<Select
361v-model="data[field]"
362:options="courses"
363option-label="label"
364option-value="value"
365/>
366</template>
367</Column>
368<Column field="semesters" header="Семестры">
369<template #body="slotProps">
370<div class="flex gap-2 flex-wrap">
371<Chip
372v-for="semester in slotProps.data.semesters"
373:label="semester.name"
374/>
375</div>
376</template>
377<template #editor="{ data, field }">
378<MultiSelect
379v-model="data.semesters"
380:max-selected-labels="2"
381display="chip"
382:options="semesters"
383option-label="name"
384filter
385placeholder="Выберите семестры"
386class="w-48"
387/>
388</template>
389</Column>
390<Column field="updated_at" header="Дата изменения" style="width: 20%">
391<template #body="slotProps">
392{{ useDateFormat(slotProps.data.updated_at, 'DD.MM.YY HH:mm:ss') }}
393</template>
394</Column>
395<Column
396:row-editor="true"
397style="width: 10%; min-width: 8rem"
398body-style="text-align:center"
399/>
400</DataTable>
401</div>
402</div>
403</template>
404