This commit is contained in:
2026-04-18 11:33:21 +03:00
parent c4e113a494
commit af757ff224
11 changed files with 753 additions and 27 deletions

View File

@@ -39,6 +39,17 @@
Users
</router-link>
<router-link
to="/restaurants"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
:class="{ 'bg-primary-50 text-primary-700': $route.path === '/restaurants' }"
>
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
Restaurants
</router-link>
<router-link
to="/settings"
class="flex items-center px-4 py-3 text-gray-700 rounded-lg hover:bg-gray-100 transition-colors"
@@ -111,28 +122,28 @@
</template>
<script setup lang="ts">
import {computed, ref} from 'vue'
import { computed, ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const userName = ref('Loading...')
const userLogin = ref('')
const userName = ref('Admin User')
const userInitials = computed(() => {
return userName.value.split(' ').map(n => n[0]).join('').toUpperCase()
})
const pageTitle = computed(() => {
const titles: Record<string, string> = {
'/dashboard': 'Dashboard',
'/users': 'Users Management',
'/settings': 'Settings'
onMounted(async () => {
try {
const res = await fetch('/api/admin/me')
if (res.ok) {
const data = await res.json()
userLogin.value = data.login
userName.value = data.login // или можно сделать красивое отображение
}
} catch (e) {
userName.value = 'User'
}
return titles[route.path] || 'Admin Panel'
})
async function logout() {
await fetch('/api/logout', { method: 'POST' })
router.push('/login')
}
const userInitials = computed(() => {
return (userName.value[0] || 'U').toUpperCase()
})
</script>

View File

@@ -2,19 +2,29 @@ import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/auth/Login.vue'
import Setup from '../views/auth/Setup.vue'
import Dashboard from '../views/Dashboard.vue'
import Users from '../views/Users.vue'
import Restaurants from '../views/Restaurants.vue'
import NotFound from '../views/NotFound.vue'
const routes = [
{ path: '/login', component: Login, meta: { title: 'Login' } },
{ path: '/setup', component: Setup, meta: { title: 'Setup' } },
{
path: '/',
redirect: '/dashboard'
},
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true, title: 'Dashboard' }
},
{
path: '/',
redirect: '/dashboard'
{ path: '/users',
component: Users,
meta: { requiresAuth: true, title: 'Users' }
},
{ path: '/restaurants',
component: Restaurants,
meta: { requiresAuth: true, title: 'Restaurants' }
},
{
path: '/:pathMatch(.*)*',
@@ -46,6 +56,18 @@ router.beforeEach(async (to, from, next) => {
console.error('Failed to check status', e)
}
if (to.path === '/login') {
try {
const meRes = await fetch('/api/admin/me');
if (meRes.ok) {
next('/dashboard');
return;
}
} catch (e) {
// игнорируем ошибку, продолжаем
}
}
// Check authentication
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)

View File

@@ -103,7 +103,7 @@
<div class="space-y-4">
<div v-for="service in systemServices" :key="service.name" class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div :class="['w-2 h-2 rounded-full', service.status === 'healthy' ? 'bg-green-500' : 'bg-red-500']"></div>
<div :class="['w-2 h-2 rounded-full', service.status === 'up' ? 'bg-green-500' : 'bg-red-500']"></div>
<span class="text-gray-700">{{ service.name }}</span>
</div>
<span class="text-sm text-gray-500">{{ service.latency }}ms</span>
@@ -115,7 +115,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import AppLayout from '../components/Layout/AppLayout.vue'
const stats = ref({
@@ -126,12 +126,34 @@ const stats = ref({
})
const recentUsers = ref([])
const systemServices = ref([
{ name: 'Database', status: 'healthy', latency: 12 },
{ name: 'Redis Cache', status: 'healthy', latency: 3 },
{ name: 'API Gateway', status: 'healthy', latency: 45 },
{ name: 'File Storage', status: 'healthy', latency: 28 }
])
const systemServices = ref([])
async function loadHealth() {
try {
const res = await fetch('/api/health')
const data = await res.json()
if (data.checks) {
systemServices.value = data.checks.map(check => ({
name: check.data?.name || check.id,
status: check.status.toLowerCase(),
latency: check.data?.latency_ms || 0
}))
}
} catch (e) {
console.error('Health check failed', e)
}
}
let interval: number
onMounted(async () => {
await loadData()
await loadHealth()
interval = window.setInterval(loadHealth, 5000)
})
onUnmounted(() => {
if (interval) clearInterval(interval)
})
onMounted(async () => {
await loadData()

View File

@@ -0,0 +1,140 @@
<template>
<AppLayout>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Restaurants</h1>
<button @click="openModal('create')" class="btn-primary">+ Add Restaurant</button>
</div>
<div class="card overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Host</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="rest in restaurants" :key="rest.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ rest.name }}</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">{{ rest.host }}</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-sm space-x-2">
<button @click="openModal('edit', rest)" class="text-blue-600 hover:text-blue-800">Edit</button>
<button @click="deleteRestaurant(rest.id)" class="text-red-600 hover:text-red-800">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal -->
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
<form @submit.prevent="submitRestaurant">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Name</label>
<input v-model="form.name" type="text" required class="input-field mt-1" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Login</label>
<input v-model="form.login" type="text" required class="input-field mt-1" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Host</label>
<input v-model="form.host" type="text" required class="input-field mt-1" />
</div>
<div class="flex justify-end space-x-2">
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AppLayout from '../components/Layout/AppLayout.vue';
const restaurants = ref([]);
const modalOpen = ref(false);
const modalMode = ref<'create' | 'edit'>('create');
const form = ref({ id: null, name: '', login: '', password: '', host: '' });
const modalTitle = ref('');
async function loadRestaurants() {
const res = await fetch('/api/admin/restaurants');
restaurants.value = await res.json();
}
function formatDate(dateStr: string) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
function openModal(mode: 'create' | 'edit', rest: any = null) {
modalMode.value = mode;
if (mode === 'create') {
form.value = { id: null, name: '', login: '', password: '', host: '' };
modalTitle.value = 'Create Restaurant';
} else {
form.value = { id: rest.id, name: rest.name, login: rest.login, password: '', host: rest.host };
modalTitle.value = 'Edit Restaurant';
}
modalOpen.value = true;
}
function closeModal() {
modalOpen.value = false;
}
async function submitRestaurant() {
try {
const payload = {
name: form.value.name,
login: form.value.login,
host: form.value.host,
...(form.value.password ? { password: form.value.password } : {})
};
if (modalMode.value === 'create') {
await fetch('/api/admin/restaurants', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} else {
await fetch(`/api/admin/restaurants/${form.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
await loadRestaurants();
closeModal();
} catch (e) {
alert('Operation failed');
}
}
async function deleteRestaurant(id: number) {
if (confirm('Are you sure?')) {
await fetch(`/api/admin/restaurants/${id}`, { method: 'DELETE' });
await loadRestaurants();
}
}
onMounted(loadRestaurants);
</script>

View File

@@ -0,0 +1,124 @@
<template>
<AppLayout>
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">Users Management</h1>
<button @click="openModal('create')" class="btn-primary">+ Add User</button>
</div>
<div class="card overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Login</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr v-for="user in users" :key="user.id">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ user.login }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ user.ip || '-' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ formatDate(user.created) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
<button @click="openModal('edit', user)" class="text-blue-600 hover:text-blue-800">Edit</button>
<button @click="deleteUser(user.id)" class="text-red-600 hover:text-red-800">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal -->
<div v-if="modalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<h2 class="text-xl font-bold mb-4">{{ modalTitle }}</h2>
<form @submit.prevent="submitUser">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Login</label>
<input v-model="form.login" type="text" required class="input-field mt-1" />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="form.password" :required="modalMode === 'create'" type="password" class="input-field mt-1" />
<p v-if="modalMode === 'edit'" class="text-xs text-gray-500 mt-1">Leave blank to keep current password</p>
</div>
<div class="flex justify-end space-x-2">
<button type="button" @click="closeModal" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</div>
</AppLayout>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AppLayout from '../components/Layout/AppLayout.vue';
const users = ref([]);
const modalOpen = ref(false);
const modalMode = ref<'create' | 'edit'>('create');
const form = ref({ id: null, login: '', password: '' });
const modalTitle = ref('');
async function loadUsers() {
const res = await fetch('/api/admin/users');
users.value = await res.json();
}
function formatDate(dateStr: string) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleString();
}
function openModal(mode: 'create' | 'edit', user: any = null) {
modalMode.value = mode;
if (mode === 'create') {
form.value = { id: null, login: '', password: '' };
modalTitle.value = 'Create User';
} else {
form.value = { id: user.id, login: user.login, password: '' };
modalTitle.value = 'Edit User';
}
modalOpen.value = true;
}
function closeModal() {
modalOpen.value = false;
}
async function submitUser() {
try {
if (modalMode.value === 'create') {
await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login: form.value.login, password: form.value.password })
});
} else {
await fetch(`/api/admin/users/${form.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ login: form.value.login, password: form.value.password || undefined })
});
}
await loadUsers();
closeModal();
} catch (e) {
alert('Operation failed');
}
}
async function deleteUser(id: number) {
if (confirm('Are you sure?')) {
await fetch(`/api/admin/users/${id}`, { method: 'DELETE' });
await loadUsers();
}
}
onMounted(loadUsers);
</script>