add: translation of OlapQueries.vue

fix: optimization of translation
This commit is contained in:
2026-05-08 15:17:17 +03:00
parent 8f86dc5831
commit 031757353d
6 changed files with 102 additions and 79 deletions

View File

@@ -87,14 +87,14 @@
route.path === '/olap/columns' ? 'bg-primary-50 text-primary-700' : 'text-gray-700', route.path === '/olap/columns' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3' sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]" ]"
:title="sidebarCollapsed ? t('app.olapColumns') : ''" :title="sidebarCollapsed ? t('olapColumns.title') : ''"
> >
<!-- OLAP Columns icon (grid / columns) --> <!-- OLAP Columns icon (grid / columns) -->
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 6v12M16 6v12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 6v12M16 6v12" />
</svg> </svg>
<span v-if="!sidebarCollapsed" class="truncate">{{ t('app.olapColumns') }}</span> <span v-if="!sidebarCollapsed" class="truncate">{{ t('olapColumns.title') }}</span>
</router-link> </router-link>
<router-link <router-link
@@ -105,14 +105,13 @@
route.path === '/olap/queries' ? 'bg-primary-50 text-primary-700' : 'text-gray-700', route.path === '/olap/queries' ? 'bg-primary-50 text-primary-700' : 'text-gray-700',
sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3' sidebarCollapsed ? 'justify-center p-2' : 'px-4 py-3 space-x-3'
]" ]"
:title="sidebarCollapsed ? 'OLAP Запросы' : ''" :title="sidebarCollapsed ? t('olapQueries.title') : ''"
> >
<!-- Queries icon (magnifying glass over database) or document with lines -->
<svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h8M8 14h5" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h8M8 14h5" />
</svg> </svg>
<span v-if="!sidebarCollapsed" class="truncate">OLAP запросы</span> <span v-if="!sidebarCollapsed" class="truncate">{{ t('olapQueries.title') }}</span>
</router-link> </router-link>
<router-link <router-link

View File

@@ -6,7 +6,6 @@
"users": "Users", "users": "Users",
"restaurants": "Restaurants", "restaurants": "Restaurants",
"settings": "Settings", "settings": "Settings",
"olapColumns": "OLAP Fields",
"profile": "Profile", "profile": "Profile",
"logout": "Logout", "logout": "Logout",
"language": "Language", "language": "Language",
@@ -14,8 +13,6 @@
"notifications": "Notifications", "notifications": "Notifications",
"administrator": "Administrator", "administrator": "Administrator",
"user": "User", "user": "User",
"yes": "Yes",
"no": "No",
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save", "save": "Save",
"delete": "Delete", "delete": "Delete",
@@ -138,7 +135,6 @@
"email": "Email Address", "email": "Email Address",
"newPassword": "New Password", "newPassword": "New Password",
"confirmPassword": "Confirm New Password", "confirmPassword": "Confirm New Password",
"language": "Language",
"save": "Save Changes", "save": "Save Changes",
"reset": "Reset", "reset": "Reset",
"role": "Role", "role": "Role",
@@ -197,8 +193,8 @@
"email": "Please enter a valid email address", "email": "Please enter a valid email address",
"passwordMismatch": "Passwords do not match" "passwordMismatch": "Passwords do not match"
}, },
"olap": { "olapColumns": {
"columnsTitle": "OLAP Reports Structure", "title": "OLAP Reports Structure",
"initialize": "Initialize", "initialize": "Initialize",
"filterFieldKey": "Field key", "filterFieldKey": "Field key",
"filterFieldKeyPlaceholder": "search by key...", "filterFieldKeyPlaceholder": "search by key...",
@@ -235,6 +231,21 @@
"deleteField": "Delete Field", "deleteField": "Delete Field",
"deleteFieldConfirm": "Are you sure you want to delete this field? This action cannot be undone." "deleteFieldConfirm": "Are you sure you want to delete this field? This action cannot be undone."
}, },
"olapQueries": {
"title": "OLAP Queries",
"createButton": "Create Query",
"lastRun": "Last Run",
"result": "Result",
"connection": "Connection",
"success": "Success",
"error": "Error",
"noQueries": "No queries. Create your first!",
"deleteQueriesTitle": "Delete query?",
"deleteQueriesMessage": "This action is irreversible. Are you sure?",
"loadError": "Failed to load queries",
"deleteSuccess": "Query deleted",
"deleteError": "Delete error"
},
"dbConnections": { "dbConnections": {
"pageName": "Databases", "pageName": "Databases",
"add": "Add Connection", "add": "Add Connection",

View File

@@ -6,7 +6,6 @@
"users": "Пользователи", "users": "Пользователи",
"restaurants": "Рестораны", "restaurants": "Рестораны",
"settings": "Настройки", "settings": "Настройки",
"olapColumns": "OLAP поля",
"profile": "Профиль", "profile": "Профиль",
"logout": "Выйти", "logout": "Выйти",
"language": "Язык", "language": "Язык",
@@ -14,8 +13,6 @@
"notifications": "Уведомления", "notifications": "Уведомления",
"administrator": "Администратор", "administrator": "Администратор",
"user": "Пользователь", "user": "Пользователь",
"yes": "Да",
"no": "Нет",
"cancel": "Отмена", "cancel": "Отмена",
"save": "Сохранить", "save": "Сохранить",
"delete": "Удалить", "delete": "Удалить",
@@ -138,7 +135,6 @@
"email": "Email", "email": "Email",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
"confirmPassword": "Подтверждение нового пароля", "confirmPassword": "Подтверждение нового пароля",
"language": "Язык",
"save": "Сохранить изменения", "save": "Сохранить изменения",
"reset": "Сбросить", "reset": "Сбросить",
"role": "Роль", "role": "Роль",
@@ -197,8 +193,8 @@
"email": "Введите корректный email адрес", "email": "Введите корректный email адрес",
"passwordMismatch": "Пароли не совпадают" "passwordMismatch": "Пароли не совпадают"
}, },
"olap": { "olapColumns": {
"columnsTitle": "Структура OLAP-отчётов", "title": "OLAP поля",
"initialize": "Инициализировать", "initialize": "Инициализировать",
"filterFieldKey": "Ключ поля", "filterFieldKey": "Ключ поля",
"filterFieldKeyPlaceholder": "поиск по ключу...", "filterFieldKeyPlaceholder": "поиск по ключу...",
@@ -235,6 +231,21 @@
"deleteField": "Удаление поля", "deleteField": "Удаление поля",
"deleteFieldConfirm": "Вы уверены, что хотите удалить это поле? Это действие необратимо." "deleteFieldConfirm": "Вы уверены, что хотите удалить это поле? Это действие необратимо."
}, },
"olapQueries": {
"title": "OLAP запросы",
"createButton": "Создать запрос",
"lastRun": "Последнее выполнение",
"result": "Результат",
"connection": "Подключение",
"success": "Успешно",
"error": "Ошибка",
"noQueries": "Нет запросов. Создайте первый!",
"deleteQueriesTitle": "Удалить запрос?",
"deleteQueriesMessage": "Действие необратимо. Вы уверены?",
"loadError": "Ошибка загрузки запросов",
"deleteSuccess": "Запрос удалён",
"deleteError": "Ошибка удаления"
},
"dbConnections": { "dbConnections": {
"pageName": "Базы данных", "pageName": "Базы данных",
"add": "Добавить подключение", "add": "Добавить подключение",

View File

@@ -1,7 +1,7 @@
<template> <template>
<AppLayout> <AppLayout>
<div class="flex justify-between items-center mb-6 flex-wrap gap-4"> <div class="flex justify-between items-center mb-6 flex-wrap gap-4">
<h1 class="text-2xl font-bold text-gray-900">{{ t('olap.columnsTitle') }}</h1> <h1 class="text-2xl font-bold text-gray-900">{{ t('olapColumns.title') }}</h1>
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
v-if="hasData && !loading && !initializing" v-if="hasData && !loading && !initializing"
@@ -11,7 +11,7 @@
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
{{ t('olap.refreshStructure') }} {{ t('olapColumns.refreshStructure') }}
</button> </button>
<button <button
v-if="!hasData && !loading && !initializing" v-if="!hasData && !loading && !initializing"
@@ -21,7 +21,7 @@
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg> </svg>
{{ t('olap.initialize') }} {{ t('olapColumns.initialize') }}
</button> </button>
</div> </div>
</div> </div>
@@ -30,18 +30,18 @@
<div v-if="hasData" class="card mb-6 p-4"> <div v-if="hasData" class="card mb-6 p-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olap.filterFieldKey') }}</label> <label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olapColumns.filterFieldKey') }}</label>
<input v-model="filters.fieldKey" type="text" class="input-field" :placeholder="t('olap.filterFieldKeyPlaceholder')" /> <input v-model="filters.fieldKey" type="text" class="input-field" :placeholder="t('olapColumns.filterFieldKeyPlaceholder')" />
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olap.filterReportType') }}</label> <label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olapColumns.filterReportType') }}</label>
<select v-model="filters.reportType" class="input-field"> <select v-model="filters.reportType" class="input-field">
<option value="">{{ t('app.all') }}</option> <option value="">{{ t('app.all') }}</option>
<option v-for="rt in availableReportTypes" :key="rt" :value="rt">{{ rt }}</option> <option v-for="rt in availableReportTypes" :key="rt" :value="rt">{{ rt }}</option>
</select> </select>
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olap.filterTag') }}</label> <label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olapColumns.filterTag') }}</label>
<select v-model="filters.tag" class="input-field"> <select v-model="filters.tag" class="input-field">
<option value="">{{ t('app.all') }}</option> <option value="">{{ t('app.all') }}</option>
<option v-for="tag in availableTags" :key="tag" :value="tag">{{ tag }}</option> <option v-for="tag in availableTags" :key="tag" :value="tag">{{ tag }}</option>
@@ -62,21 +62,21 @@
<p class="mt-2 text-gray-500">{{ t('app.loading') }}</p> <p class="mt-2 text-gray-500">{{ t('app.loading') }}</p>
</div> </div>
<div v-else-if="hasData && filteredColumns.length === 0" class="card p-8 text-center text-gray-500"> <div v-else-if="hasData && filteredColumns.length === 0" class="card p-8 text-center text-gray-500">
{{ t('olap.noColumnsFound') }} {{ t('olapColumns.noColumnsFound') }}
</div> </div>
<div v-else-if="hasData" class="card overflow-hidden"> <div v-else-if="hasData" class="card overflow-hidden">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.fieldKey') }}</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.fieldKey') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.name') }}</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.reportTypes') }}</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.reportTypes') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.type') }}</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.type') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.tags') }}</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.tags') }}</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.aggregation') }}</th> <th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.aggregation') }}</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.grouping') }}</th> <th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.grouping') }}</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olap.filtering') }}</th> <th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapColumns.filtering') }}</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.actions') }}</th> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.actions') }}</th>
</tr> </tr>
</thead> </thead>
@@ -162,7 +162,7 @@
v-model="restaurantSearch" v-model="restaurantSearch"
type="text" type="text"
class="input-field pl-9" class="input-field pl-9"
:placeholder="t('olap.searchRestaurant')" :placeholder="t('olapColumns.searchRestaurant')"
/> />
</div> </div>
</div> </div>
@@ -198,7 +198,7 @@
</div> </div>
</div> </div>
<div v-if="filteredRestaurants.length === 0" class="text-center py-8 text-gray-500"> <div v-if="filteredRestaurants.length === 0" class="text-center py-8 text-gray-500">
{{ t('olap.noRestaurantsFound') }} {{ t('olapColumns.noRestaurantsFound') }}
</div> </div>
</div> </div>
@@ -230,11 +230,11 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
</div> </div>
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('olap.refreshWarningTitle') }}</h3> <h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('olapColumns.refreshWarningTitle') }}</h3>
<p class="text-sm text-gray-500 mb-4"> <p class="text-sm text-gray-500 mb-4">
{{ t('olap.refreshWarningMessage', { restaurant: pendingRestaurantName }) }} {{ t('olapColumns.refreshWarningMessage', { restaurant: pendingRestaurantName }) }}
</p> </p>
<p class="text-sm font-semibold text-red-600 mb-6">{{ t('olap.refreshWarningConfirm') }}</p> <p class="text-sm font-semibold text-red-600 mb-6">{{ t('olapColumns.refreshWarningConfirm') }}</p>
<div class="flex justify-center space-x-3"> <div class="flex justify-center space-x-3">
<button @click="refreshWarningModal.show = false" class="btn-secondary">{{ t('app.cancel') }}</button> <button @click="refreshWarningModal.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="executeInitialize" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700"> <button @click="executeInitialize" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">
@@ -254,7 +254,7 @@
<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"> <div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full">
<div class="flex justify-between items-center p-6 border-b"> <div class="flex justify-between items-center p-6 border-b">
<h2 class="text-xl font-bold text-gray-900">{{ t('olap.editField') }}</h2> <h2 class="text-xl font-bold text-gray-900">{{ t('olapColumns.editField') }}</h2>
<button @click="closeEditModal" class="text-gray-400 hover:text-gray-600"> <button @click="closeEditModal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -267,7 +267,7 @@
<input v-model="editForm.name" type="text" class="input-field" required /> <input v-model="editForm.name" type="text" class="input-field" required />
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olap.displayType') }}</label> <label class="block text-sm font-medium text-gray-700 mb-1">{{ t('olapColumns.displayType') }}</label>
<select v-model="editForm.typeNormal" class="input-field"> <select v-model="editForm.typeNormal" class="input-field">
<option value="string">string</option> <option value="string">string</option>
<option value="integer">integer</option> <option value="integer">integer</option>
@@ -277,15 +277,15 @@
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" v-model="editForm.aggregationAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" /> <input type="checkbox" v-model="editForm.aggregationAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" />
<label class="text-sm font-medium text-gray-700">{{ t('olap.aggregation') }}</label> <label class="text-sm font-medium text-gray-700">{{ t('olapColumns.aggregation') }}</label>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" v-model="editForm.groupingAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" /> <input type="checkbox" v-model="editForm.groupingAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" />
<label class="text-sm font-medium text-gray-700">{{ t('olap.grouping') }}</label> <label class="text-sm font-medium text-gray-700">{{ t('olapColumns.grouping') }}</label>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<input type="checkbox" v-model="editForm.filteringAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" /> <input type="checkbox" v-model="editForm.filteringAllowed" class="rounded border-gray-300 text-primary-600 focus:ring-primary-500 w-4 h-4 mr-2" />
<label class="text-sm font-medium text-gray-700">{{ t('olap.filtering') }}</label> <label class="text-sm font-medium text-gray-700">{{ t('olapColumns.filtering') }}</label>
</div> </div>
<div class="flex justify-end space-x-3 pt-2"> <div class="flex justify-end space-x-3 pt-2">
<button type="button" @click="closeEditModal" class="btn-secondary">{{ t('app.cancel') }}</button> <button type="button" @click="closeEditModal" class="btn-secondary">{{ t('app.cancel') }}</button>
@@ -309,8 +309,8 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
</div> </div>
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('olap.deleteField') }}</h3> <h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('olapColumns.deleteField') }}</h3>
<p class="text-sm text-gray-500 mb-6">{{ t('olap.deleteFieldConfirm') }}</p> <p class="text-sm text-gray-500 mb-6">{{ t('olapColumns.deleteFieldConfirm') }}</p>
<div class="flex justify-center space-x-3"> <div class="flex justify-center space-x-3">
<button @click="deleteFieldConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button> <button @click="deleteFieldConfirm.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmDeleteField" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">{{ t('app.delete') }}</button> <button @click="confirmDeleteField" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">{{ t('app.delete') }}</button>
@@ -332,7 +332,7 @@
{{ initializingText }} {{ initializingText }}
</p> </p>
<p class="text-sm text-gray-500 text-center"> <p class="text-sm text-gray-500 text-center">
{{ t('olap.waitMessage') }} {{ t('olapColumns.waitMessage') }}
</p> </p>
</div> </div>
</div> </div>
@@ -453,7 +453,7 @@ async function loadColumns() {
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showNotification('olap.loadError', 'error'); showNotification('olapColumns.loadError', 'error');
columns.value = []; columns.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
@@ -486,12 +486,12 @@ const filteredRestaurants = computed(() => {
}); });
function openInitModal() { function openInitModal() {
initModalTitle.value = t('olap.selectRestaurant'); initModalTitle.value = t('olapColumns.selectRestaurant');
loadRestaurants().then(() => { initModalOpen.value = true; }); loadRestaurants().then(() => { initModalOpen.value = true; });
} }
function openRefreshModal() { function openRefreshModal() {
initModalTitle.value = t('olap.refreshStructure'); initModalTitle.value = t('olapColumns.refreshStructure');
loadRestaurants().then(() => { initModalOpen.value = true; }); loadRestaurants().then(() => { initModalOpen.value = true; });
} }
@@ -517,7 +517,7 @@ function onInitConfirm() {
async function executeInitialize() { async function executeInitialize() {
const id = pendingRestaurantId.value ?? selectedRestaurantId.value; const id = pendingRestaurantId.value ?? selectedRestaurantId.value;
if (!id) { if (!id) {
showNotification('olap.selectRestaurantFirst', 'error'); showNotification('olapColumns.selectRestaurantFirst', 'error');
return; return;
} }
@@ -526,7 +526,7 @@ async function executeInitialize() {
editModalOpen.value = false; editModalOpen.value = false;
deleteFieldConfirm.value.show = false; deleteFieldConfirm.value.show = false;
initializingText.value = hasData.value ? t('olap.refreshingData') : t('olap.initializingData'); initializingText.value = hasData.value ? t('olapColumns.refreshingData') : t('olapColumns.initializingData');
initializing.value = true; initializing.value = true;
try { try {
@@ -539,10 +539,10 @@ async function executeInitialize() {
const errText = await res.text(); const errText = await res.text();
throw new Error(errText || `HTTP ${res.status}`); throw new Error(errText || `HTTP ${res.status}`);
} }
showNotification('olap.initSuccess', 'success'); showNotification('olapColumns.initSuccess', 'success');
await loadColumns(); await loadColumns();
} catch (error: any) { } catch (error: any) {
showNotification('olap.initError', 'error', { error: error.message }); showNotification('olapColumns.initError', 'error', { error: error.message });
} finally { } finally {
initializing.value = false; initializing.value = false;
initializingText.value = ''; initializingText.value = '';
@@ -583,11 +583,11 @@ async function updateField() {
}) })
}); });
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
showNotification('olap.updateSuccess', 'success'); showNotification('olapColumns.updateSuccess', 'success');
closeEditModal(); closeEditModal();
await loadColumns(); await loadColumns();
} catch (error) { } catch (error) {
showNotification('olap.updateError', 'error'); showNotification('olapColumns.updateError', 'error');
} }
} }
@@ -603,11 +603,11 @@ async function confirmDeleteField() {
method: 'DELETE' method: 'DELETE'
}); });
if (!res.ok) throw new Error(); if (!res.ok) throw new Error();
showNotification('olap.deleteSuccess', 'success'); showNotification('olapColumns.deleteSuccess', 'success');
deleteFieldConfirm.value.show = false; deleteFieldConfirm.value.show = false;
await loadColumns(); await loadColumns();
} catch (error) { } catch (error) {
showNotification('olap.deleteError', 'error'); showNotification('olapColumns.deleteError', 'error');
} }
} }

View File

@@ -1354,7 +1354,7 @@ onMounted(async () => {
isDirty.value = false isDirty.value = false
} }
triggerSqlUpdate() triggerSqlUpdate()
isReady.value = true // <- Страница полностью загружена isReady.value = true
}) })
</script> </script>

View File

@@ -1,8 +1,8 @@
<template> <template>
<AppLayout> <AppLayout>
<div class="flex justify-between items-center mb-6"> <div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-900">OLAP запросы</h1> <h1 class="text-2xl font-bold text-gray-900">{{ t('olapQueries.title') }}</h1>
<router-link to="/olap/constructor" class="btn-primary">+ Создать запрос</router-link> <router-link to="/olap/constructor" class="btn-primary">+ {{ t('olapQueries.createButton') }}</router-link>
</div> </div>
<div class="card overflow-hidden"> <div class="card overflow-hidden">
@@ -10,15 +10,15 @@
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.id') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Название</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.name') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Активен</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.active') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Последнее выполнение</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapQueries.lastRun') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Результат</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapQueries.result') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Подключение</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('olapQueries.connection') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Рестораны</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.restaurants') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Создан</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.created') }}</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Действия</th> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.actions') }}</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 bg-white">
@@ -27,14 +27,14 @@
<td class="px-6 py-4 text-sm font-medium">{{ q.name }}</td> <td class="px-6 py-4 text-sm font-medium">{{ q.name }}</td>
<td class="px-6 py-4 text-sm"> <td class="px-6 py-4 text-sm">
<span :class="q.active ? 'text-green-600' : 'text-red-600'"> <span :class="q.active ? 'text-green-600' : 'text-red-600'">
{{ q.active ? 'Да' : 'Нет' }} {{ q.active ? t('common.yes') : t('common.no') }}
</span> </span>
</td> </td>
<td class="px-6 py-4 text-sm">{{ q.lastRun ? formatDate(q.lastRun) : '—' }}</td> <td class="px-6 py-4 text-sm">{{ q.lastRun ? formatDate(q.lastRun) : '—' }}</td>
<td class="px-6 py-4 text-sm"> <td class="px-6 py-4 text-sm">
<span v-if="q.lastRunSuccess === null"></span> <span v-if="q.lastRunSuccess === null"></span>
<span v-else-if="q.lastRunSuccess" class="text-green-600">Успешно</span> <span v-else-if="q.lastRunSuccess" class="text-green-600">{{ t('olapQueries.success') }}</span>
<span v-else class="text-red-600">Ошибка</span> <span v-else class="text-red-600">{{ t('olapQueries.error') }}</span>
</td> </td>
<td class="px-6 py-4 text-sm">{{ q.dbConnectionName }}</td> <td class="px-6 py-4 text-sm">{{ q.dbConnectionName }}</td>
<td class="px-6 py-4 text-sm">{{ q.restaurants }}</td> <td class="px-6 py-4 text-sm">{{ q.restaurants }}</td>
@@ -53,7 +53,7 @@
</td> </td>
</tr> </tr>
<tr v-if="queries.length === 0"> <tr v-if="queries.length === 0">
<td colspan="6" class="px-6 py-12 text-center text-gray-500">Нет запросов. Создайте первый!</td> <td colspan="6" class="px-6 py-12 text-center text-gray-500">{{ t('olapQueries.noQueries') }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -73,11 +73,11 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
</div> </div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Удалить запрос?</h3> <h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('olapQueries.deleteQueriesTitle') }}</h3>
<p class="text-sm text-gray-500 mb-6">Действие необратимо. Вы уверены?</p> <p class="text-sm text-gray-500 mb-6">{{ t('olapQueries.deleteQueriesMessage') }}</p>
<div class="flex justify-center space-x-3"> <div class="flex justify-center space-x-3">
<button @click="deleteModal.show = false" class="btn-secondary">Отмена</button> <button @click="deleteModal.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="deleteQuery" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors">Удалить</button> <button @click="deleteQuery" 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> </div>
@@ -90,9 +90,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import AppLayout from '@/components/Layout/AppLayout.vue' import AppLayout from '@/components/Layout/AppLayout.vue'
import { useNotification } from '@/composables/useNotification' import { useNotification } from '@/composables/useNotification'
const { t } = useI18n()
const { showNotification } = useNotification() const { showNotification } = useNotification()
const queries = ref([]) const queries = ref([])
const deleteModal = ref({ show: false, id: null as number | null }) const deleteModal = ref({ show: false, id: null as number | null })
@@ -103,7 +105,7 @@ async function loadQueries() {
if (!res.ok) throw new Error() if (!res.ok) throw new Error()
queries.value = await res.json() queries.value = await res.json()
} catch (e) { } catch (e) {
showNotification('Ошибка загрузки запросов', 'error') showNotification('olapQueries.loadError', 'error')
} }
} }
@@ -121,10 +123,10 @@ async function deleteQuery() {
try { try {
const res = await fetch(`/api/olap/queries/${id}`, { method: 'DELETE' }) const res = await fetch(`/api/olap/queries/${id}`, { method: 'DELETE' })
if (!res.ok) throw new Error() if (!res.ok) throw new Error()
showNotification('Запрос удалён', 'success') showNotification('olapQueries.deleteSuccess', 'success')
await loadQueries() await loadQueries()
} catch (e) { } catch (e) {
showNotification('Ошибка удаления', 'error') showNotification('olapQueries.deleteError', 'error')
} finally { } finally {
deleteModal.value.show = false deleteModal.value.show = false
} }