fix frontend
This commit is contained in:
@@ -191,6 +191,18 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
<!-- Notification Toast -->
|
||||||
|
<Transition name="slide">
|
||||||
|
<div v-if="notification.show" class="fixed bottom-4 right-4 z-50 flex items-center space-x-2 px-4 py-3 rounded-lg shadow-lg" :class="notification.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'">
|
||||||
|
<svg v-if="notification.type === 'success'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ notification.message }}</span>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -200,7 +212,9 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useSettingsStore } from '../../stores/settings'
|
import { useSettingsStore } from '../../stores/settings'
|
||||||
import { useUserStore } from '../../stores/user'
|
import { useUserStore } from '../../stores/user'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useNotification } from '../../composables/useNotification'
|
||||||
|
|
||||||
|
const { notification } = useNotification()
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -237,6 +251,7 @@ async function toggleLanguage() {
|
|||||||
locale.value = newLang
|
locale.value = newLang
|
||||||
localStorage.setItem('locale', newLang)
|
localStorage.setItem('locale', newLang)
|
||||||
} else {
|
} else {
|
||||||
|
showNotification('profile.updateError', 'error');
|
||||||
// В случае ошибки всё равно меняем локаль, но не сохраняем в БД
|
// В случае ошибки всё равно меняем локаль, но не сохраняем в БД
|
||||||
locale.value = newLang
|
locale.value = newLang
|
||||||
localStorage.setItem('locale', newLang)
|
localStorage.setItem('locale', newLang)
|
||||||
@@ -248,3 +263,15 @@ async function toggleLanguage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.slide-enter-active,
|
||||||
|
.slide-leave-active {
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.slide-enter-from,
|
||||||
|
.slide-leave-to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
43
frontend/src/composables/useNotification.ts
Normal file
43
frontend/src/composables/useNotification.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ref, readonly } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
type NotificationType = 'success' | 'error'
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
show: boolean
|
||||||
|
type: NotificationType
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = ref<Notification>({
|
||||||
|
show: false,
|
||||||
|
type: 'success',
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
let timeoutId: number | null = null
|
||||||
|
|
||||||
|
export function useNotification() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const showNotification = (messageKey: string, type: NotificationType = 'success', params?: Record<string, any>) => {
|
||||||
|
const message = params ? t(messageKey, params) : t(messageKey)
|
||||||
|
|
||||||
|
// Очищаем предыдущий таймер, чтобы уведомление не закрылось раньше времени
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
timeoutId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.value = { show: true, type, message }
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
notification.value.show = false
|
||||||
|
timeoutId = null
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
notification: readonly(notification),
|
||||||
|
showNotification
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,8 @@
|
|||||||
"leavePasswordBlank": "Leave blank to keep current password",
|
"leavePasswordBlank": "Leave blank to keep current password",
|
||||||
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
"deleteConfirmation": "Are you sure you want to delete this item? This action cannot be undone.",
|
||||||
"operationSuccess": "Operation completed successfully",
|
"operationSuccess": "Operation completed successfully",
|
||||||
"operationFailed": "Operation failed"
|
"operationFailed": "Operation failed",
|
||||||
|
"networkError": "Network error"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"totalUsers": "Total Users",
|
"totalUsers": "Total Users",
|
||||||
@@ -68,7 +69,8 @@
|
|||||||
"new": "New",
|
"new": "New",
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"yesterday": "Yesterday",
|
"yesterday": "Yesterday",
|
||||||
"daysAgo": "{count} days ago"
|
"daysAgo": "{count} days ago",
|
||||||
|
"loadError": "Failed to load dashboard data"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"pageName": "Users Management",
|
"pageName": "Users Management",
|
||||||
@@ -76,9 +78,21 @@
|
|||||||
"edit": "Edit User",
|
"edit": "Edit User",
|
||||||
"delete": "Delete User",
|
"delete": "Delete User",
|
||||||
"you": "(You)",
|
"you": "(You)",
|
||||||
"cannotChangeOwnRole": "You cannot change your own role",
|
|
||||||
"confirmDelete": "Delete User",
|
"confirmDelete": "Delete User",
|
||||||
"deleteConfirmation": "Are you sure you want to delete this user? This action cannot be undone."
|
"deleteConfirmation": "Are you sure you want to delete this user? This action cannot be undone.",
|
||||||
|
"statusUpdated": "User status updated",
|
||||||
|
"statusUpdateError": "Failed to update user status",
|
||||||
|
"passwordRequired": "Password is required for new user",
|
||||||
|
"createSuccess": "User created successfully",
|
||||||
|
"createError": "Failed to create user",
|
||||||
|
"updateSuccess": "User updated successfully",
|
||||||
|
"updateError": "Failed to update user",
|
||||||
|
"deleteSuccess": "User deleted",
|
||||||
|
"deleteError": "Failed to delete user",
|
||||||
|
"cannotChangeOwnRole": "You cannot change your own role",
|
||||||
|
"noUsers": "No users found",
|
||||||
|
"loadError": "Failed to load users",
|
||||||
|
"loadCurrentError": "Failed to load current user info"
|
||||||
},
|
},
|
||||||
"restaurants": {
|
"restaurants": {
|
||||||
"pageName": "Restaurants",
|
"pageName": "Restaurants",
|
||||||
@@ -92,7 +106,7 @@
|
|||||||
"noRestaurants": "No restaurants found. Click \"Add Restaurant\" to create one.",
|
"noRestaurants": "No restaurants found. Click \"Add Restaurant\" to create one.",
|
||||||
"deleteConfirmation": "Are you sure you want to delete this restaurant? This action cannot be undone.",
|
"deleteConfirmation": "Are you sure you want to delete this restaurant? This action cannot be undone.",
|
||||||
"check": "Check connection",
|
"check": "Check connection",
|
||||||
"checkError": "Error",
|
"checkError": "Check failed: {error}",
|
||||||
"loadError": "Failed to load restaurants",
|
"loadError": "Failed to load restaurants",
|
||||||
"createSuccess": "Restaurant created successfully",
|
"createSuccess": "Restaurant created successfully",
|
||||||
"updateSuccess": "Restaurant updated successfully",
|
"updateSuccess": "Restaurant updated successfully",
|
||||||
@@ -109,7 +123,10 @@
|
|||||||
"saved": "Settings saved successfully",
|
"saved": "Settings saved successfully",
|
||||||
"saveFailed": "Failed to save settings",
|
"saveFailed": "Failed to save settings",
|
||||||
"loadFailed": "Failed to load settings metadata",
|
"loadFailed": "Failed to load settings metadata",
|
||||||
"enabled": "Enabled"
|
"enabled": "Enabled",
|
||||||
|
"saveSuccess": "Settings saved successfully",
|
||||||
|
"saveError": "Failed to save settings",
|
||||||
|
"loadMetaError": "Failed to load settings metadata"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "My Profile",
|
"title": "My Profile",
|
||||||
@@ -122,9 +139,9 @@
|
|||||||
"save": "Save Changes",
|
"save": "Save Changes",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"passwordMismatch": "Passwords do not match",
|
"passwordsMismatch": "Passwords do not match",
|
||||||
"updateSuccess": "Profile updated successfully",
|
"updateSuccess": "Profile updated successfully",
|
||||||
"updateFailed": "Failed to update profile"
|
"updateError": "Failed to update profile"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Welcome Back",
|
"title": "Welcome Back",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"leavePasswordBlank": "Оставьте пустым, чтобы оставить текущий пароль",
|
"leavePasswordBlank": "Оставьте пустым, чтобы оставить текущий пароль",
|
||||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
|
"deleteConfirmation": "Вы уверены, что хотите удалить этот элемент? Это действие необратимо.",
|
||||||
"operationSuccess": "Операция выполнена успешно",
|
"operationSuccess": "Операция выполнена успешно",
|
||||||
"operationFailed": "Операция не удалась"
|
"operationFailed": "Операция не удалась",
|
||||||
|
"networkError": "Ошибка сети"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"totalUsers": "Всего пользователей",
|
"totalUsers": "Всего пользователей",
|
||||||
@@ -68,7 +69,8 @@
|
|||||||
"new": "Новый",
|
"new": "Новый",
|
||||||
"today": "Сегодня",
|
"today": "Сегодня",
|
||||||
"yesterday": "Вчера",
|
"yesterday": "Вчера",
|
||||||
"daysAgo": "дн. назад"
|
"daysAgo": "дн. назад",
|
||||||
|
"loadError": "Ошибка загрузки данных дашборда"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"pageName": "Управление пользователями",
|
"pageName": "Управление пользователями",
|
||||||
@@ -76,9 +78,21 @@
|
|||||||
"edit": "Редактировать пользователя",
|
"edit": "Редактировать пользователя",
|
||||||
"delete": "Удалить пользователя",
|
"delete": "Удалить пользователя",
|
||||||
"you": "(Вы)",
|
"you": "(Вы)",
|
||||||
"cannotChangeOwnRole": "Вы не можете изменить свою собственную роль",
|
|
||||||
"confirmDelete": "Удалить пользователя",
|
"confirmDelete": "Удалить пользователя",
|
||||||
"deleteConfirmation": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо."
|
"deleteConfirmation": "Вы уверены, что хотите удалить этого пользователя? Это действие необратимо.",
|
||||||
|
"statusUpdated": "Статус пользователя обновлён",
|
||||||
|
"statusUpdateError": "Не удалось обновить статус",
|
||||||
|
"passwordRequired": "Пароль обязателен для нового пользователя",
|
||||||
|
"createSuccess": "Пользователь создан",
|
||||||
|
"createError": "Ошибка создания пользователя",
|
||||||
|
"updateSuccess": "Пользователь обновлён",
|
||||||
|
"updateError": "Ошибка обновления пользователя",
|
||||||
|
"deleteSuccess": "Пользователь удалён",
|
||||||
|
"deleteError": "Ошибка удаления пользователя",
|
||||||
|
"cannotChangeOwnRole": "Вы не можете изменить свою роль",
|
||||||
|
"noUsers": "Пользователи не найдены",
|
||||||
|
"loadError": "Ошибка загрузки списка пользователей",
|
||||||
|
"loadCurrentError": "Ошибка загрузки информации о текущем пользователе"
|
||||||
},
|
},
|
||||||
"restaurants": {
|
"restaurants": {
|
||||||
"pageName": "Рестораны",
|
"pageName": "Рестораны",
|
||||||
@@ -92,7 +106,7 @@
|
|||||||
"noRestaurants": "Ресторанов не найдено. Нажмите \"Добавить ресторан\", чтобы создать его.",
|
"noRestaurants": "Ресторанов не найдено. Нажмите \"Добавить ресторан\", чтобы создать его.",
|
||||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо.",
|
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо.",
|
||||||
"check": "Проверить подключение",
|
"check": "Проверить подключение",
|
||||||
"checkError": "Ошибка",
|
"checkError": "Ошибка проверки: {error}",
|
||||||
"loadError": "Ошибка загрузки списка ресторанов",
|
"loadError": "Ошибка загрузки списка ресторанов",
|
||||||
"createSuccess": "Ресторан успешно создан",
|
"createSuccess": "Ресторан успешно создан",
|
||||||
"updateSuccess": "Ресторан успешно обновлён",
|
"updateSuccess": "Ресторан успешно обновлён",
|
||||||
@@ -109,7 +123,10 @@
|
|||||||
"saved": "Настройки успешно сохранены",
|
"saved": "Настройки успешно сохранены",
|
||||||
"saveFailed": "Не удалось сохранить настройки",
|
"saveFailed": "Не удалось сохранить настройки",
|
||||||
"loadFailed": "Не удалось загрузить метаданные настроек",
|
"loadFailed": "Не удалось загрузить метаданные настроек",
|
||||||
"enabled": "Включено"
|
"enabled": "Включено",
|
||||||
|
"saveSuccess": "Настройки сохранены",
|
||||||
|
"saveError": "Ошибка сохранения настроек",
|
||||||
|
"loadMetaError": "Ошибка загрузки метаданных"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Мой профиль",
|
"title": "Мой профиль",
|
||||||
@@ -122,9 +139,9 @@
|
|||||||
"save": "Сохранить изменения",
|
"save": "Сохранить изменения",
|
||||||
"reset": "Сбросить",
|
"reset": "Сбросить",
|
||||||
"role": "Роль",
|
"role": "Роль",
|
||||||
"passwordMismatch": "Пароли не совпадают",
|
"passwordsMismatch": "Пароли не совпадают",
|
||||||
"updateSuccess": "Профиль успешно обновлен",
|
"updateSuccess": "Профиль обновлён",
|
||||||
"updateFailed": "Не удалось обновить профиль"
|
"updateError": "Ошибка обновления профиля"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "С возвращением",
|
"title": "С возвращением",
|
||||||
|
|||||||
@@ -70,7 +70,9 @@
|
|||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useNotification } from '../composables/useNotification'
|
||||||
|
|
||||||
|
const { showNotification } = useNotification()
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
interface FieldMeta {
|
interface FieldMeta {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -92,7 +94,7 @@ async function loadMeta() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
meta.value = await res.json();
|
meta.value = await res.json();
|
||||||
} else {
|
} else {
|
||||||
showMessage('Failed to load settings metadata', 'bg-red-50 text-red-800');
|
showNotification('settings.loadMetaError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +103,7 @@ async function loadValues() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
values.value = await res.json();
|
values.value = await res.json();
|
||||||
} else {
|
} else {
|
||||||
showMessage('Failed to load settings values', 'bg-red-50 text-red-800');
|
showNotification('settings.loadMetaError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +119,12 @@ async function saveSettings() {
|
|||||||
body: JSON.stringify(values.value),
|
body: JSON.stringify(values.value),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
showMessage('Settings saved successfully', 'bg-green-50 text-green-800');
|
showNotification('settings.saveSuccess', 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage('Failed to save settings', 'bg-red-50 text-red-800');
|
showNotification('settings.saveError', 'error');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showMessage('Network error', 'bg-red-50 text-red-800');
|
showNotification('common.networkError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ import { ref, onMounted, onUnmounted } from 'vue';
|
|||||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
import { useNotification } from '../composables/useNotification'
|
||||||
|
|
||||||
|
const { showNotification } = useNotification()
|
||||||
const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' });
|
const stats = ref({ totalUsers: 0, activeSessions: 0, systemHealth: 100, uptime: '99.9%' });
|
||||||
const userGrowth = ref(12);
|
const userGrowth = ref(12);
|
||||||
const sessionGrowth = ref(5);
|
const sessionGrowth = ref(5);
|
||||||
@@ -222,6 +224,7 @@ async function loadDashboardData() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
showNotification('dashboard.loadError', 'error');
|
||||||
console.error('Failed to load dashboard data', e);
|
console.error('Failed to load dashboard data', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,19 +77,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Notification Toast -->
|
|
||||||
<Transition name="slide">
|
|
||||||
<div v-if="notification.show" class="fixed bottom-4 right-4 z-50 flex items-center space-x-2 px-4 py-3 rounded-lg shadow-lg" :class="notification.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'">
|
|
||||||
<svg v-if="notification.type === 'success'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>{{ notification.message }}</span>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
@@ -112,18 +99,10 @@ const form = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const notification = ref({ show: false, type: 'success', message: '' });
|
|
||||||
|
|
||||||
const userInitials = computed(() => (userStore.login[0] || 'U').toUpperCase());
|
const userInitials = computed(() => (userStore.login[0] || 'U').toUpperCase());
|
||||||
const passwordMismatch = computed(() => !!form.password && form.password !== form.confirmPassword);
|
const passwordMismatch = computed(() => !!form.password && form.password !== form.confirmPassword);
|
||||||
|
|
||||||
function showNotification(message: string, type: 'success' | 'error') {
|
|
||||||
notification.value = { show: true, type, message };
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.value.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
form.email = userStore.email;
|
form.email = userStore.email;
|
||||||
form.password = '';
|
form.password = '';
|
||||||
@@ -133,7 +112,7 @@ function resetForm() {
|
|||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
if (form.password && form.password !== form.confirmPassword) {
|
if (form.password && form.password !== form.confirmPassword) {
|
||||||
showNotification('Passwords do not match', 'error');
|
showNotification('profile.passwordsMismatch', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,10 +128,10 @@ async function saveProfile() {
|
|||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
locale.value = form.language;
|
locale.value = form.language;
|
||||||
showNotification('Profile updated successfully', 'success');
|
showNotification('profile.updateSuccess', 'success');
|
||||||
resetForm(); // очищаем поля пароля
|
resetForm(); // очищаем поля пароля
|
||||||
} else {
|
} else {
|
||||||
showNotification('Failed to update profile', 'error');
|
showNotification('profile.updateError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,15 +140,3 @@ onMounted(() => {
|
|||||||
form.language = userStore.language;
|
form.language = userStore.language;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.slide-enter-active,
|
|
||||||
.slide-leave-active {
|
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.slide-enter-from,
|
|
||||||
.slide-leave-to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -31,12 +31,7 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.host }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.host }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
<label class="relative inline-flex items-center cursor-pointer">
|
<label class="relative inline-flex items-center cursor-pointer">
|
||||||
<input
|
<input type="checkbox" :checked="rest.https" @change="toggleHttps(rest)" class="sr-only peer" />
|
||||||
type="checkbox"
|
|
||||||
:checked="rest.https"
|
|
||||||
@change="toggleHttps(rest)"
|
|
||||||
class="sr-only peer"
|
|
||||||
/>
|
|
||||||
<div class="w-9 h-5 bg-gray-200 rounded-full peer peer-checked:bg-primary-600 transition-colors"></div>
|
<div class="w-9 h-5 bg-gray-200 rounded-full peer peer-checked:bg-primary-600 transition-colors"></div>
|
||||||
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
<div class="absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||||||
</label>
|
</label>
|
||||||
@@ -45,13 +40,7 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(rest.created) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div class="flex items-center justify-end space-x-3">
|
<div class="flex items-center justify-end space-x-3">
|
||||||
<!-- Кнопка проверки -->
|
<button @click="checkRestaurant(rest)" :disabled="rest.checking" class="text-green-600 hover:text-green-800 transition-colors disabled:opacity-50">
|
||||||
<button
|
|
||||||
@click="checkRestaurant(rest)"
|
|
||||||
:disabled="rest.checking"
|
|
||||||
class="text-green-600 hover:text-green-800 transition-colors disabled:opacity-50"
|
|
||||||
:title="t('restaurants.check')"
|
|
||||||
>
|
|
||||||
<svg v-if="!rest.checking" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg v-if="!rest.checking" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -60,20 +49,17 @@
|
|||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Кнопка редактирования -->
|
|
||||||
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Кнопка удаления -->
|
|
||||||
<button @click="confirmDelete(rest.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
<button @click="confirmDelete(rest.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<!-- Результат проверки только при успехе (ms) -->
|
<span v-if="rest.checkResult" class="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700 ml-1 whitespace-nowrap">
|
||||||
<span v-if="rest.checkResult" class="text-xs text-gray-500 ml-1 whitespace-nowrap">
|
|
||||||
{{ rest.checkResult }}
|
{{ rest.checkResult }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,28 +148,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<!-- Уведомления (toast) -->
|
|
||||||
<Transition name="slide">
|
|
||||||
<div v-if="notification.show" class="fixed bottom-4 right-4 z-50 flex items-center space-x-2 px-4 py-3 rounded-lg shadow-lg" :class="notification.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'">
|
|
||||||
<svg v-if="notification.type === 'success'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>{{ notification.message }}</span>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useNotification } from '../composables/useNotification';
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n();
|
||||||
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
type Restaurant = {
|
type Restaurant = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -203,29 +178,18 @@ const form = ref({ id: null, name: '', login: '', password: '', host: '', https:
|
|||||||
const modalTitle = ref('');
|
const modalTitle = ref('');
|
||||||
const deleteConfirm = ref({ show: false, id: null });
|
const deleteConfirm = ref({ show: false, id: null });
|
||||||
|
|
||||||
// Уведомления
|
|
||||||
const notification = ref({ show: false, type: 'success' as 'success' | 'error', message: '' });
|
|
||||||
|
|
||||||
function showNotification(message: string, type: 'success' | 'error') {
|
|
||||||
notification.value = { show: true, type, message };
|
|
||||||
setTimeout(() => {
|
|
||||||
notification.value.show = false;
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadRestaurants() {
|
async function loadRestaurants() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/restaurants');
|
const res = await fetch('/api/admin/restaurants');
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
restaurants.value = data.map((r: any) => ({
|
restaurants.value = data.map((r: any) => ({
|
||||||
...r,
|
...r,
|
||||||
checking: false,
|
checking: false,
|
||||||
checkResult: null
|
checkResult: null
|
||||||
}));
|
}));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
showNotification(t('restaurants.loadError'), 'error');
|
showNotification('restaurants.loadError', 'error');
|
||||||
restaurants.value = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,53 +200,25 @@ function formatDate(dateStr: string) {
|
|||||||
|
|
||||||
async function checkRestaurant(rest: Restaurant) {
|
async function checkRestaurant(rest: Restaurant) {
|
||||||
rest.checking = true;
|
rest.checking = true;
|
||||||
rest.checkResult = null; // сбрасываем
|
rest.checkResult = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/admin/restaurants/${rest.id}/check`);
|
const response = await fetch(`/api/admin/restaurants/${rest.id}/check`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
rest.checkResult = `${data.latency_ms} ms`; // показываем в таблице
|
rest.checkResult = `${data.latency_ms} ms`;
|
||||||
} else {
|
} else {
|
||||||
const errorText = data.error || 'Unknown error';
|
const errorText = data.error || 'Unknown error';
|
||||||
showNotification(`${t('restaurants.checkError')}: ${errorText}`, 'error');
|
showNotification('restaurants.checkError', 'error', { error: errorText });
|
||||||
// rest.checkResult остаётся null -> ничего не показываем
|
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const msg = t('restaurants.checkNetworkError');
|
showNotification('restaurants.checkNetworkError', 'error', { error: error.message });
|
||||||
showNotification(`${msg}: ${error.message}`, 'error');
|
|
||||||
// rest.checkResult остаётся null
|
|
||||||
} finally {
|
} finally {
|
||||||
rest.checking = false;
|
rest.checking = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal(mode: 'create' | 'edit', rest: Restaurant | null = null) {
|
|
||||||
modalMode.value = mode;
|
|
||||||
if (mode === 'create') {
|
|
||||||
form.value = { id: null, name: '', login: '', password: '', host: '', https: false };
|
|
||||||
modalTitle.value = t('restaurants.add');
|
|
||||||
} else {
|
|
||||||
if (rest) {
|
|
||||||
form.value = {
|
|
||||||
id: rest.id,
|
|
||||||
name: rest.name,
|
|
||||||
login: rest.login,
|
|
||||||
password: '',
|
|
||||||
host: rest.host,
|
|
||||||
https: rest.https || false
|
|
||||||
};
|
|
||||||
modalTitle.value = t('restaurants.edit');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modalOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
modalOpen.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleHttps(rest: Restaurant) {
|
async function toggleHttps(rest: Restaurant) {
|
||||||
const newHttps = !rest.https;
|
const newHttps = !rest.https;
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -299,19 +235,41 @@ async function toggleHttps(rest: Restaurant) {
|
|||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
rest.https = newHttps;
|
rest.https = newHttps;
|
||||||
showNotification(t('restaurants.httpsUpdateSuccess'), 'success');
|
showNotification('restaurants.httpsUpdateSuccess', 'success');
|
||||||
} else {
|
} else {
|
||||||
const errText = await res.text();
|
showNotification('restaurants.httpsUpdateError', 'error');
|
||||||
showNotification(`${t('restaurants.httpsUpdateError')}: ${errText}`, 'error');
|
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
showNotification(`${t('restaurants.httpsUpdateError')}: ${e.message}`, 'error');
|
showNotification('restaurants.httpsUpdateError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openModal(mode: 'create' | 'edit', rest: Restaurant | null = null) {
|
||||||
|
modalMode.value = mode;
|
||||||
|
if (mode === 'create') {
|
||||||
|
form.value = { id: null, name: '', login: '', password: '', host: '', https: false };
|
||||||
|
modalTitle.value = t('restaurants.add');
|
||||||
|
} else if (rest) {
|
||||||
|
form.value = {
|
||||||
|
id: rest.id,
|
||||||
|
name: rest.name,
|
||||||
|
login: rest.login,
|
||||||
|
password: '',
|
||||||
|
host: rest.host,
|
||||||
|
https: rest.https || false
|
||||||
|
};
|
||||||
|
modalTitle.value = t('restaurants.edit');
|
||||||
|
}
|
||||||
|
modalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modalOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
async function submitRestaurant() {
|
async function submitRestaurant() {
|
||||||
if (modalMode.value === 'create' && !form.value.password) {
|
if (modalMode.value === 'create' && !form.value.password) {
|
||||||
showNotification(t('restaurants.passwordRequired'), 'error');
|
showNotification('restaurants.passwordRequired', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,28 +281,27 @@ async function submitRestaurant() {
|
|||||||
login: form.value.login,
|
login: form.value.login,
|
||||||
...(form.value.password ? { password: form.value.password } : {})
|
...(form.value.password ? { password: form.value.password } : {})
|
||||||
};
|
};
|
||||||
let response;
|
|
||||||
if (modalMode.value === 'create') {
|
if (modalMode.value === 'create') {
|
||||||
response = await fetch('/api/admin/restaurants', {
|
const res = await fetch('/api/admin/restaurants', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Create failed');
|
if (!res.ok) throw new Error();
|
||||||
showNotification(t('restaurants.createSuccess'), 'success');
|
showNotification('restaurants.createSuccess', 'success');
|
||||||
} else {
|
} else {
|
||||||
response = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
const res = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Update failed');
|
if (!res.ok) throw new Error();
|
||||||
showNotification(t('restaurants.updateSuccess'), 'success');
|
showNotification('restaurants.updateSuccess', 'success');
|
||||||
}
|
}
|
||||||
await loadRestaurants();
|
await loadRestaurants();
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
showNotification(e.message, 'error');
|
showNotification(modalMode.value === 'create' ? 'restaurants.createError' : 'restaurants.updateError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,11 +312,11 @@ function confirmDelete(id: number) {
|
|||||||
async function deleteRestaurant(id: number) {
|
async function deleteRestaurant(id: number) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
const res = await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
||||||
if (!res.ok) throw new Error('Delete failed');
|
if (!res.ok) throw new Error();
|
||||||
showNotification(t('restaurants.deleteSuccess'), 'success');
|
showNotification('restaurants.deleteSuccess', 'success');
|
||||||
await loadRestaurants();
|
await loadRestaurants();
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
showNotification(e.message, 'error');
|
showNotification('restaurants.deleteError', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
deleteConfirm.value.show = false;
|
deleteConfirm.value.show = false;
|
||||||
}
|
}
|
||||||
@@ -383,13 +340,4 @@ onMounted(loadRestaurants);
|
|||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.slide-enter-active,
|
|
||||||
.slide-leave-active {
|
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.slide-enter-from,
|
|
||||||
.slide-leave-to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -68,19 +68,17 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="users.length === 0">
|
<tr v-if="users.length === 0">
|
||||||
<td colspan="8" class="px-6 py-12 text-center text-gray-500">No users found. Click "Add User" to create one.</td>
|
<td colspan="8" class="px-6 py-12 text-center text-gray-500">{{ t('users.noUsers') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Модалка создания/редактирования пользователя -->
|
<!-- Modal for create/edit user -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeModal">
|
<div v-if="modalOpen" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeModal">
|
||||||
<!-- Затемнение с размытием -->
|
|
||||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||||
|
|
||||||
<div class="flex items-center justify-center min-h-screen p-4">
|
<div class="flex items-center justify-center min-h-screen p-4">
|
||||||
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full transform transition-all">
|
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full transform transition-all">
|
||||||
<div class="flex justify-between items-center p-6 border-b">
|
<div class="flex justify-between items-center p-6 border-b">
|
||||||
@@ -129,7 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<!-- Модалка подтверждения удаления -->
|
<!-- Delete confirmation modal -->
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div v-if="deleteConfirm.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="deleteConfirm.show = false">
|
<div v-if="deleteConfirm.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="deleteConfirm.show = false">
|
||||||
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
|
||||||
@@ -159,26 +157,15 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import AppLayout from '../components/Layout/AppLayout.vue';
|
import AppLayout from '../components/Layout/AppLayout.vue';
|
||||||
import { useUserStore } from '../stores/user';
|
import { useUserStore } from '../stores/user';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useNotification } from '../composables/useNotification';
|
||||||
const { t, locale } = useI18n()
|
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { showNotification } = useNotification();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const currentUserId = ref<number | null>(null);
|
const currentUserId = ref<number | null>(null);
|
||||||
|
const users = ref<any[]>([]);
|
||||||
async function loadCurrentUser() {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/admin/me');
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
currentUserId.value = data.id;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load current user', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = ref([]);
|
|
||||||
const modalOpen = ref(false);
|
const modalOpen = ref(false);
|
||||||
const modalMode = ref<'create' | 'edit'>('create');
|
const modalMode = ref<'create' | 'edit'>('create');
|
||||||
const form = ref({ id: null, login: '', email: '', password: '', role: 'user' });
|
const form = ref({ id: null, login: '', email: '', password: '', role: 'user' });
|
||||||
@@ -189,9 +176,28 @@ const isEditingSelf = computed(() => {
|
|||||||
return modalMode.value === 'edit' && form.value.id === currentUserId.value;
|
return modalMode.value === 'edit' && form.value.id === currentUserId.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadCurrentUser() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/admin/me');
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
currentUserId.value = data.id;
|
||||||
|
} else {
|
||||||
|
showNotification('users.loadCurrentError', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('common.networkError', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
|
try {
|
||||||
const res = await fetch('/api/admin/users');
|
const res = await fetch('/api/admin/users');
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
users.value = await res.json();
|
users.value = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('users.loadError', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateStr: string) {
|
function formatDate(dateStr: string) {
|
||||||
@@ -200,8 +206,14 @@ function formatDate(dateStr: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(user: any) {
|
async function toggleActive(user: any) {
|
||||||
await fetch(`/api/admin/users/${user.id}/activate?active=${!user.active}`, { method: 'PUT' });
|
try {
|
||||||
|
const res = await fetch(`/api/admin/users/${user.id}/activate?active=${!user.active}`, { method: 'PUT' });
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
|
showNotification('users.statusUpdated', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('users.statusUpdateError', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openModal(mode: 'create' | 'edit', user: any = null) {
|
function openModal(mode: 'create' | 'edit', user: any = null) {
|
||||||
@@ -222,7 +234,12 @@ function closeModal() {
|
|||||||
|
|
||||||
async function submitUser() {
|
async function submitUser() {
|
||||||
if (isEditingSelf.value && form.value.role !== userStore.role) {
|
if (isEditingSelf.value && form.value.role !== userStore.role) {
|
||||||
alert('You cannot change your own role');
|
showNotification('users.cannotChangeOwnRole', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalMode.value === 'create' && !form.value.password) {
|
||||||
|
showNotification('users.passwordRequired', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,29 +252,30 @@ async function submitUser() {
|
|||||||
if (form.value.password) {
|
if (form.value.password) {
|
||||||
payload.password = form.value.password;
|
payload.password = form.value.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response;
|
||||||
if (modalMode.value === 'create') {
|
if (modalMode.value === 'create') {
|
||||||
if (!form.value.password) {
|
response = await fetch('/api/admin/users', {
|
||||||
alert('Password is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await fetch('/api/admin/users', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Create failed');
|
if (!response.ok) throw new Error();
|
||||||
|
showNotification('users.createSuccess', 'success');
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(`/api/admin/users/${form.value.id}`, {
|
response = await fetch(`/api/admin/users/${form.value.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Update failed');
|
if (!response.ok) throw new Error();
|
||||||
|
showNotification('users.updateSuccess', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
closeModal();
|
closeModal();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Operation failed: ' + e.message);
|
showNotification(modalMode.value === 'create' ? 'users.createError' : 'users.updateError', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,10 +284,17 @@ function confirmDelete(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteUser(id: number) {
|
async function deleteUser(id: number) {
|
||||||
await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
try {
|
||||||
|
const res = await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error();
|
||||||
|
showNotification('users.deleteSuccess', 'success');
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('users.deleteError', 'error');
|
||||||
|
} finally {
|
||||||
deleteConfirm.value.show = false;
|
deleteConfirm.value.show = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadCurrentUser();
|
await loadCurrentUser();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /phpmyadmin {
|
location /phpmyadmin/ {
|
||||||
allow 80.68.9.83;
|
allow 80.68.9.83;
|
||||||
allow 185.51.125.202;
|
allow 185.51.125.202;
|
||||||
|
|
||||||
@@ -64,10 +64,12 @@ server {
|
|||||||
|
|
||||||
deny all;
|
deny all;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:7102;
|
proxy_pass http://127.0.0.1:7102/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection keep-alive;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_cache_bypass $http_upgrade;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user