add restaurants check connection
This commit is contained in:
@@ -90,7 +90,17 @@
|
||||
"useHttps": "Use HTTPS",
|
||||
"confirmDelete": "Delete Restaurant",
|
||||
"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",
|
||||
"checkError": "Error",
|
||||
"loadError": "Failed to load restaurants",
|
||||
"createSuccess": "Restaurant created successfully",
|
||||
"updateSuccess": "Restaurant updated successfully",
|
||||
"deleteSuccess": "Restaurant deleted",
|
||||
"httpsUpdateSuccess": "HTTPS status updated",
|
||||
"httpsUpdateError": "Failed to update HTTPS",
|
||||
"passwordRequired": "Password is required for new restaurant",
|
||||
"checkNetworkError": "Network error while checking"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Application Settings",
|
||||
|
||||
@@ -90,7 +90,17 @@
|
||||
"useHttps": "Использовать HTTPS",
|
||||
"confirmDelete": "Удалить ресторан",
|
||||
"noRestaurants": "Ресторанов не найдено. Нажмите \"Добавить ресторан\", чтобы создать его.",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо."
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить этот ресторан? Это действие необратимо.",
|
||||
"check": "Проверить подключение",
|
||||
"checkError": "Ошибка",
|
||||
"loadError": "Ошибка загрузки списка ресторанов",
|
||||
"createSuccess": "Ресторан успешно создан",
|
||||
"updateSuccess": "Ресторан успешно обновлён",
|
||||
"deleteSuccess": "Ресторан удалён",
|
||||
"httpsUpdateSuccess": "Статус HTTPS обновлён",
|
||||
"httpsUpdateError": "Не удалось обновить HTTPS",
|
||||
"passwordRequired": "Пароль обязателен для нового ресторана",
|
||||
"checkNetworkError": "Ошибка сети при проверке"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки приложения",
|
||||
|
||||
@@ -43,17 +43,40 @@
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ rest.login }}</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 space-x-3">
|
||||
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800 transition-colors">
|
||||
<svg class="w-5 h-5 inline" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="confirmDelete(rest.id)" class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="w-5 h-5 inline" 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" />
|
||||
</svg>
|
||||
</button>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<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"
|
||||
:title="t('restaurants.check')"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
<svg v-else class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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>
|
||||
</button>
|
||||
<!-- Кнопка редактирования -->
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
<!-- Кнопка удаления -->
|
||||
<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">
|
||||
<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>
|
||||
</button>
|
||||
<!-- Результат проверки только при успехе (ms) -->
|
||||
<span v-if="rest.checkResult" class="text-xs text-gray-500 ml-1 whitespace-nowrap">
|
||||
{{ rest.checkResult }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="restaurants.length === 0">
|
||||
@@ -64,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модалка создания/редактирования ресторана -->
|
||||
<!-- Модалка создания/редактирования (без изменений) -->
|
||||
<Transition name="fade">
|
||||
<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>
|
||||
@@ -116,7 +139,7 @@
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Модалка подтверждения удаления ресторана -->
|
||||
<!-- Модалка подтверждения удаления -->
|
||||
<Transition name="fade">
|
||||
<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>
|
||||
@@ -132,13 +155,26 @@
|
||||
<p class="text-sm text-gray-500 mb-6">{{ t('restaurants.deleteConfirmation') }}</p>
|
||||
<div class="flex justify-center space-x-3">
|
||||
<button @click="deleteConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
|
||||
<button @click="deleteUser(deleteConfirm.id)" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">{{ t('app.delete') }}</button>
|
||||
<button @click="deleteRestaurant(deleteConfirm.id)" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">{{ t('app.delete') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -148,16 +184,49 @@ import AppLayout from '../components/Layout/AppLayout.vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const restaurants = ref([]);
|
||||
|
||||
type Restaurant = {
|
||||
id: number;
|
||||
name: string;
|
||||
host: string;
|
||||
https: boolean;
|
||||
login: string;
|
||||
created: string;
|
||||
checking?: boolean;
|
||||
checkResult?: string | null;
|
||||
};
|
||||
|
||||
const restaurants = ref<Restaurant[]>([]);
|
||||
const modalOpen = ref(false);
|
||||
const modalMode = ref<'create' | 'edit'>('create');
|
||||
const form = ref({ id: null, name: '', login: '', password: '', host: '', https: false });
|
||||
const modalTitle = ref('');
|
||||
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() {
|
||||
const res = await fetch('/api/admin/restaurants');
|
||||
restaurants.value = await res.json();
|
||||
try {
|
||||
const res = await fetch('/api/admin/restaurants');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
restaurants.value = data.map((r: any) => ({
|
||||
...r,
|
||||
checking: false,
|
||||
checkResult: null
|
||||
}));
|
||||
} catch (e: any) {
|
||||
showNotification(t('restaurants.loadError'), 'error');
|
||||
restaurants.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string) {
|
||||
@@ -165,21 +234,47 @@ function formatDate(dateStr: string) {
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
function openModal(mode: 'create' | 'edit', rest: any = null) {
|
||||
async function checkRestaurant(rest: Restaurant) {
|
||||
rest.checking = true;
|
||||
rest.checkResult = null; // сбрасываем
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/restaurants/${rest.id}/check`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
rest.checkResult = `${data.latency_ms} ms`; // показываем в таблице
|
||||
} else {
|
||||
const errorText = data.error || 'Unknown error';
|
||||
showNotification(`${t('restaurants.checkError')}: ${errorText}`, 'error');
|
||||
// rest.checkResult остаётся null -> ничего не показываем
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = t('restaurants.checkNetworkError');
|
||||
showNotification(`${msg}: ${error.message}`, 'error');
|
||||
// rest.checkResult остаётся null
|
||||
} finally {
|
||||
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 {
|
||||
form.value = {
|
||||
id: rest.id,
|
||||
name: rest.name,
|
||||
login: rest.login,
|
||||
password: '',
|
||||
host: rest.host,
|
||||
https: rest.https || false
|
||||
};
|
||||
modalTitle.value = t('restaurants.edit');
|
||||
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;
|
||||
}
|
||||
@@ -188,7 +283,7 @@ function closeModal() {
|
||||
modalOpen.value = false;
|
||||
}
|
||||
|
||||
async function toggleHttps(rest: any) {
|
||||
async function toggleHttps(rest: Restaurant) {
|
||||
const newHttps = !rest.https;
|
||||
const payload = {
|
||||
name: rest.name,
|
||||
@@ -204,15 +299,22 @@ async function toggleHttps(rest: any) {
|
||||
});
|
||||
if (res.ok) {
|
||||
rest.https = newHttps;
|
||||
showNotification(t('restaurants.httpsUpdateSuccess'), 'success');
|
||||
} else {
|
||||
alert('Failed to update HTTPS status');
|
||||
const errText = await res.text();
|
||||
showNotification(`${t('restaurants.httpsUpdateError')}: ${errText}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Network error');
|
||||
} catch (e: any) {
|
||||
showNotification(`${t('restaurants.httpsUpdateError')}: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRestaurant() {
|
||||
if (modalMode.value === 'create' && !form.value.password) {
|
||||
showNotification(t('restaurants.passwordRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name: form.value.name,
|
||||
@@ -221,29 +323,28 @@ async function submitRestaurant() {
|
||||
login: form.value.login,
|
||||
...(form.value.password ? { password: form.value.password } : {})
|
||||
};
|
||||
let response;
|
||||
if (modalMode.value === 'create') {
|
||||
if (!form.value.password) {
|
||||
alert('Password is required');
|
||||
return;
|
||||
}
|
||||
const res = await fetch('/api/admin/restaurants', {
|
||||
response = await fetch('/api/admin/restaurants', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Create failed');
|
||||
if (!response.ok) throw new Error('Create failed');
|
||||
showNotification(t('restaurants.createSuccess'), 'success');
|
||||
} else {
|
||||
const res = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||
response = await fetch(`/api/admin/restaurants/${form.value.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!res.ok) throw new Error('Update failed');
|
||||
if (!response.ok) throw new Error('Update failed');
|
||||
showNotification(t('restaurants.updateSuccess'), 'success');
|
||||
}
|
||||
await loadRestaurants();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
alert('Operation failed: ' + e.message);
|
||||
} catch (e: any) {
|
||||
showNotification(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,15 +353,28 @@ function confirmDelete(id: number) {
|
||||
}
|
||||
|
||||
async function deleteRestaurant(id: number) {
|
||||
await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
||||
await loadRestaurants();
|
||||
deleteConfirm.value.show = false;
|
||||
try {
|
||||
const res = await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
|
||||
if (!res.ok) throw new Error('Delete failed');
|
||||
showNotification(t('restaurants.deleteSuccess'), 'success');
|
||||
await loadRestaurants();
|
||||
} catch (e: any) {
|
||||
showNotification(e.message, 'error');
|
||||
} finally {
|
||||
deleteConfirm.value.show = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadRestaurants);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
@@ -269,4 +383,13 @@ onMounted(loadRestaurants);
|
||||
.fade-leave-to {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user