diff --git a/frontend/src/features/sidebar/ui/admin/ReportsSection.vue b/frontend/src/features/sidebar/ui/admin/ReportsSection.vue index 0e60e7c..25e8732 100644 --- a/frontend/src/features/sidebar/ui/admin/ReportsSection.vue +++ b/frontend/src/features/sidebar/ui/admin/ReportsSection.vue @@ -82,9 +82,13 @@ layout="total, sizes, prev, pager, next" > +
+ Экспортировать в JSON +
+ @@ -143,6 +147,11 @@ :page-sizes="[5,10,20,50]" layout="total, sizes, prev, pager, next" > + + +
+ Экспортировать в JSON +
@@ -235,8 +244,8 @@ const fetchDataAbonements = () => { return { id: item.client.id, clientName: item.client.name + ' ' + item.client.surname, - startDate: item.startDate ? item.startDate : 'Не указано', - endDate: item.endDate ? item.endDate : 'Не указано', + startDate: item.startDate ? dayjs(item.startDate).format('DD.MM.YYYY') : 'Не указано', + endDate: item.endDate ? dayjs(item.endDate).format('DD.MM.YYYY') : 'Не указано', status: item.status } }); @@ -318,7 +327,7 @@ const fetchDataTrainings = () => { axiosInstance.get('/admins/statistics/trainings', { params: requestParamsTrainings.value }) .then(res => { const data = res.data; - totalTrainings.value = data.count; + totalTrainings.value = data.length; trainingsData.value = data.map((d:any) => { return { ...d, @@ -361,6 +370,26 @@ const loadClients = () => { }); }; +// Функция экспорта данных абонементов в JSON +const exportAbonementsToJson = () => { + const dataStr = JSON.stringify(subscriptions.value, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'abonements_data.json'; + link.click(); +}; + +// Функция экспорта данных по тренингам в JSON +const exportTrainingsToJson = () => { + const dataStr = JSON.stringify(trainingsData.value, null, 2); + const blob = new Blob([dataStr], { type: 'application/json' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = 'trainings_data.json'; + link.click(); +}; + onMounted(() => { loadClients(); const savedTab = localStorage.getItem('currentTab'); diff --git a/frontend/src/features/sidebar/ui/admin/ServiceInfoSection.vue b/frontend/src/features/sidebar/ui/admin/ServiceInfoSection.vue index 643ee27..6c6c726 100644 --- a/frontend/src/features/sidebar/ui/admin/ServiceInfoSection.vue +++ b/frontend/src/features/sidebar/ui/admin/ServiceInfoSection.vue @@ -5,7 +5,6 @@ {{ isFilterOpened ? 'Закрыть фильтрацию и поиск' : 'Открыть фильтрацию и поиск' }} - @@ -87,24 +86,18 @@ Сбросить - - - - - - - - - - - + +
Название: {{ room.name }}
+
Емкость: {{ room.capacity }}
+
Адрес: {{ room.address }}
+
Рабочие дни: {{ room.workingDays }}
+
Часы открытия: {{ room.openingTime }}
+
Часы закрытия: {{ room.closingTime }}
+
Тренеры: {{ room.trainers }}
+
Секции: {{ room.sections }}
+
-
+
- - - - {{ isFilterOpened ? 'Закрыть фильтрацию и поиск' : 'Открыть фильтрацию и поиск' }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Сбросить фильтры - - - - @@ -113,7 +23,7 @@
Подробнее - Удалить + Удалить @@ -127,10 +37,11 @@ import UserInfoSection from '@/features/sidebar/ui/admin/sections/UserInfoSectio import Spacer from '@/shared/components/Spacer.vue'; import axiosInstance from '@/widgets/axios/index.ts'; import { onMounted, ref, computed } from 'vue'; +import { ElNotification } from 'element-plus'; -const isLoading = ref(true) +const isLoading = ref(true); const userInfoSectionClicked = ref(false); -const clickedUser = ref(); +const clickedUser = ref(null); const users = ref([]); const isFilterOpened = ref(false); @@ -190,13 +101,13 @@ onMounted(() => { users.value = res.data; }).catch(error => { console.error('Ошибка при загрузке пользователей:', error); + }).finally(() => { + isLoading.value = false; }); - - isLoading.value = false; }); // Фильтрация и сортировка -const filteredSortedUsers = computed(() => { +const filteredSortedUsers = computed(() => { let filtered = users.value; // Фильтрация по каждому полю @@ -298,8 +209,60 @@ const filteredSortedUsers = computed(() => { return filtered; }); - - +// Функция для удаления пользователя +const handleUserDelete = (user) => { + if (user.roles.includes('ROLE_USER')) { + // Для пользователей с ролью ROLE_USER проверяем статус абонемента + axiosInstance.get(`/clients/${user.id}/subscription`).then((res) => { + const subscription = res.data; + if (subscription.status === 'active') { + ElNotification({ + title: 'Ошибка', + message: 'У этого пользователя есть активный абонемент. Удаление невозможно.', + type: 'error' + }); + } else { + deleteUser(user); + } + }).catch((error) => { + ElNotification({ + title: 'Ошибка', + message: error.response.data?.errors || 'Неизвестная ошибка', + type: 'error' + }); + }); + } else { + deleteUser(user); + } +}; + +// Функция для отправки запроса на удаление пользователя +const deleteUser = (user) => { + let deleteUrl = ''; + + if (user.roles.includes('ROLE_USER')) { + deleteUrl = `/clients/${user.id}`; + } else if (user.roles.includes('ROLE_TRAINER')) { + deleteUrl = `/trainers/${user.id}`; + } else if (user.roles.includes('ROLE_ADMIN')) { + deleteUrl = `/admins/${user.id}`; + } + + axiosInstance.delete(deleteUrl).then(() => { + ElNotification({ + title: 'Успех', + message: `Пользователь ${user.name} ${user.surname} успешно удалён.`, + type: 'success' + }); + // Обновить список пользователей + users.value = users.value.filter(u => u.id !== user.id); + }).catch((error) => { + ElNotification({ + title: 'Ошибка', + message: error.response.data?.errors || 'Неизвестная ошибка', + type: 'error' + }); + }); +}; + diff --git a/frontend/src/features/sidebar/ui/client/AbonementSection.vue b/frontend/src/features/sidebar/ui/client/AbonementSection.vue index 4bc2eee..2bf972a 100644 --- a/frontend/src/features/sidebar/ui/client/AbonementSection.vue +++ b/frontend/src/features/sidebar/ui/client/AbonementSection.vue @@ -2,11 +2,9 @@

Информация об абонементе

- +

- Статус: + Статус: Дата начала: {{ formatDate(userAbonementInfo.startDate) }} + Дата начала: {{ formatDate(userAbonementInfo.startDate) }}

- Дата окончания: {{ formatDate(userAbonementInfo.endDate) }} + Дата окончания: {{ formatDate(userAbonementInfo.endDate) }}

- Оставшиеся дни: {{ userAbonementInfo.restDays }} + Оставшиеся дни: {{ userAbonementInfo.restDays }}

- Длительность: {{ userAbonementInfo.duration }} дней + Длительность: {{ userAbonementInfo.duration }} дней

@@ -38,12 +36,9 @@
+ - +

Продление абонемента

@@ -68,9 +63,7 @@ - + {{ userAbonementInfo.status === 'FREEZE' ? 'Вы уверены что хотите разморозить абонемент?' : 'Вы уверены что хотите заморозить абонемент?' }} @@ -172,6 +165,17 @@ const submitExtend = () => { }; const submitFreeze = () => { + // Проверяем, если оставшиеся дни меньше или равны 7 + if (userAbonementInfo.value?.restDays <= 7) { + ElNotification({ + title: 'Ошибка', + message: 'Заморозка недоступна, так как абонемент истекает менее чем через 7 дней.', + type: 'warning', + duration: 3000 + }); + return; // Не отправляем запрос + } + axiosInstance .put(`/subscriptions/${userInfo.value?.id}/freeze`) .then((res) => { diff --git a/frontend/src/features/sidebar/ui/client/TrainersAndSections.vue b/frontend/src/features/sidebar/ui/client/TrainersAndSections.vue index 64b7981..73d3087 100644 --- a/frontend/src/features/sidebar/ui/client/TrainersAndSections.vue +++ b/frontend/src/features/sidebar/ui/client/TrainersAndSections.vue @@ -62,7 +62,25 @@ display: 'grid', gridTemplateColumns: '1fr auto' }"> - +
+ {{ `${trainer.name} ${trainer.surname}` }} + Подробнее + + + + Специализация {{ trainer.sections.join(', ') }} + + + + Квалификация: {{ trainer.qualification }} + + + + Связь {{ trainer.phoneNumber ?? trainer.email }} + + +
+ + diff --git a/frontend/src/features/sidebar/ui/trainer/TrainReportsSection.vue b/frontend/src/features/sidebar/ui/trainer/TrainReportsSection.vue index e32fef8..fe8dfd2 100644 --- a/frontend/src/features/sidebar/ui/trainer/TrainReportsSection.vue +++ b/frontend/src/features/sidebar/ui/trainer/TrainReportsSection.vue @@ -1,39 +1,192 @@ + + \ No newline at end of file + diff --git a/frontend/src/features/sidebar/ui/trainer/components/TrainerInfo.vue b/frontend/src/features/sidebar/ui/trainer/components/TrainerInfo.vue index 8dc1508..70fa539 100644 --- a/frontend/src/features/sidebar/ui/trainer/components/TrainerInfo.vue +++ b/frontend/src/features/sidebar/ui/trainer/components/TrainerInfo.vue @@ -1,41 +1,71 @@ \ No newline at end of file + ul { + margin: 10px 0; + padding-left: 20px; + + li { + margin: 5px 0; + } + } +} + diff --git a/frontend/src/pages/Login/LoginPage.vue b/frontend/src/pages/Login/LoginPage.vue index 48422b7..adaaff6 100644 --- a/frontend/src/pages/Login/LoginPage.vue +++ b/frontend/src/pages/Login/LoginPage.vue @@ -46,6 +46,7 @@ diff --git a/frontend/src/shared/components/modals/AssignToTrainModal.vue b/frontend/src/shared/components/modals/AssignToTrainModal.vue index 0dc4f55..7b90517 100644 --- a/frontend/src/shared/components/modals/AssignToTrainModal.vue +++ b/frontend/src/shared/components/modals/AssignToTrainModal.vue @@ -106,12 +106,41 @@ const emits = defineEmits([ 'close' ]); const selectedTrainingId = ref(null); // Хранит id выбранной тренировки // Функция для обработки подтверждения записи на тренировку +import { ElNotification } from 'element-plus'; + const handleConfirmAssignToTraining = (training: ITraining) => { - axiosInstance.post(`/trainings/${training.id}/registration/${userInfo.value?.id}`).then(() => { - alert('Запись успешна'); - emits('close') - }); -} + axiosInstance.post(`/trainings/${training.id}/registration/${userInfo.value?.id}`) + .then(() => { + ElNotification.success({ + title: 'Успех', + message: 'Запись успешна', + duration: 3000, // Продолжительность уведомления (в миллисекундах) + }); + emits('close'); + }) + .catch((error) => { + if (error.response) { + ElNotification.error({ + title: 'Ошибка', + message: error.response.data?.errors || 'Неизвестная ошибка', + duration: 3000, + }); + } else if (error.request) { + ElNotification.error({ + title: 'Ошибка сети', + message: 'Нет ответа от сервера', + duration: 3000, + }); + } else { + ElNotification.error({ + title: 'Ошибка', + message: error.message, + duration: 3000, + }); + } + }); +}; + // Переключение состояния выбора тренировки const toggleSelect = (trainingId: string) => { diff --git a/frontend/src/shared/components/modals/TrainerInfoModal.vue b/frontend/src/shared/components/modals/TrainerInfoModal.vue new file mode 100644 index 0000000..d6a0efd --- /dev/null +++ b/frontend/src/shared/components/modals/TrainerInfoModal.vue @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file