add: translation of OlapConstructor.vue

This commit is contained in:
2026-05-09 13:02:40 +03:00
parent 5382488a82
commit f3b407e1ed
4 changed files with 276 additions and 90 deletions

View File

@@ -2,26 +2,26 @@
<AppLayout>
<div class="flex justify-between items-center mb-6 flex-wrap gap-4">
<h1 class="text-2xl font-bold text-gray-900">
{{ queryId ? 'Редактирование запроса' : 'Новый OLAP запрос' }}
{{ queryId ? t('OlapConstructor.titleEdit') : t('OlapConstructor.titleNew') }}
</h1>
<div class="flex gap-3">
<button @click="saveQuery" class="btn-primary flex items-center gap-2">
<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="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
</svg>
Сохранить запрос
{{ t('OlapConstructor.saveQuery') }}
</button>
<button @click="exportConfigAsJson" class="btn-secondary flex items-center gap-2">
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Экспорт JSON
{{ t('OlapConstructor.exportJson') }}
</button>
<label class="btn-secondary flex items-center gap-2 cursor-pointer">
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Импорт JSON
{{ t('OlapConstructor.importJson') }}
<input type="file" accept=".json" @change="importConfigFromJson" class="hidden">
</label>
<button @click="openExitModal" class="btn-danger flex items-center gap-2">
@@ -35,20 +35,22 @@
<div class="flex flex-row gap-6">
<!-- Левая панель - поля -->
<div class="w-72 shrink-0 card p-4 flex flex-col lg:sticky lg:top-4 h-[calc(100vh-12rem)]">
<h3 class="text-lg font-bold mb-3">Поля</h3>
<h3 class="text-lg font-bold mb-3">{{ t('OlapConstructor.fieldsTitle') }}</h3>
<div class="mb-2">
<input type="text" v-model="searchQuery" placeholder="Поиск по названию или тегам" class="input-field py-1.5 text-sm">
<div class="text-xs text-gray-500 mt-1" v-if="searchQuery">Найдено: {{ filteredAvailableFields.length }} / {{ availableFields.length }}</div>
<input type="text" v-model="searchQuery" :placeholder="t('OlapConstructor.searchPlaceholder')" class="input-field py-1.5 text-sm">
<div class="text-xs text-gray-500 mt-1" v-if="searchQuery">
{{ t('OlapConstructor.foundCount', { found: filteredAvailableFields.length, total: availableFields.length }) }}
</div>
</div>
<div v-if="loading" class="text-center py-8">Загрузка полей...</div>
<div v-if="loading" class="text-center py-8">{{ t('OlapConstructor.loadingFields') }}</div>
<div v-else-if="error" class="bg-red-50 text-red-700 p-3 rounded-xl text-sm">{{ error }}</div>
<div v-else-if="availableFields.length === 0" class="text-center py-8 text-gray-500">Нет полей. Сначала инициализируйте структуру в разделе OLAP Columns.</div>
<div v-else-if="availableFields.length === 0" class="text-center py-8 text-gray-500">{{ t('OlapConstructor.noFields') }}</div>
<div v-else class="flex-1 overflow-y-auto space-y-3 pr-1">
<!-- Числовые -->
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('number')">
<h4 class="text-gray-600 text-sm font-bold">Числовые VALUES</h4>
<h4 class="text-gray-600 text-sm font-bold">{{ t('OlapConstructor.numericGroup') }}</h4>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.number" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -70,7 +72,7 @@
<!-- Категории -->
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('category')">
<h4 class="text-gray-600 text-sm font-bold">Категории ROW / COLUMN</h4>
<h4 class="text-gray-600 text-sm font-bold">{{ t('OlapConstructor.categoryGroup') }}</h4>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.category" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -92,7 +94,7 @@
<!-- Фильтры -->
<div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('filter')">
<h4 class="text-gray-600 text-sm font-bold">Фильтры</h4>
<h4 class="text-gray-600 text-sm font-bold">{{ t('OlapConstructor.filtersGroup') }}</h4>
<button class="text-gray-500 hover:text-gray-700">
<svg v-if="collapsed.filter" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -113,7 +115,7 @@
</div>
<div class="mt-3">
<button @click="openResetModal" class="btn-secondary w-full py-1.5 text-sm">Сбросить всё</button>
<button @click="openResetModal" class="btn-secondary w-full py-1.5 text-sm">{{ t('OlapConstructor.resetAll') }}</button>
</div>
</div>
@@ -122,38 +124,38 @@
<!-- Имя запроса отдельной строкой -->
<div class="mb-4">
<label class="text-sm font-medium text-gray-700">Имя запроса *</label>
<input type="text" v-model="queryName" placeholder="Например: Продажи по дням" class="input-field w-full" />
<label class="text-sm font-medium text-gray-700">{{ t('OlapConstructor.queryNameLabel') }}</label>
<input type="text" v-model="queryName" :placeholder="t('OlapConstructor.queryNamePlaceholder')" class="input-field w-full" />
</div>
<div class="card p-3 mb-4">
<div class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
<!-- Тип отчета -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Тип отчета</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.reportTypeLabel') }}</label>
<select v-model="reportType" class="input-field py-1 text-sm">
<option value="SALES">SALES</option>
<option value="DELIVERIES">DELIVERIES</option>
<option value="TRANSACTIONS">TRANSACTIONS</option>
<option value="SALES">{{ t('OlapConstructor.reportTypes.SALES') }}</option>
<option value="DELIVERIES">{{ t('OlapConstructor.reportTypes.DELIVERIES') }}</option>
<option value="TRANSACTIONS">{{ t('OlapConstructor.reportTypes.TRANSACTIONS') }}</option>
</select>
</div>
<!-- Таблица SQL -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Таблица SQL *</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.sqlTableLabel') }}</label>
<input type="text" v-model="tableName" @input="validateTableName" class="input-field py-1 text-sm"
:class="{ 'border-green-500 bg-green-50': tableNameValid && tableName, 'border-red-500 bg-red-50': tableNameTouched && !tableNameValid && tableName }" />
</div>
<!-- Дата до -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Дата до (конец дня)</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.dateToLabel') }}</label>
<input type="date" v-model="dateTo" class="input-field py-1 text-sm" />
</div>
<!-- Дней назад -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Дней назад (1)</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.daysBackLabel') }}</label>
<input type="number" v-model.number="daysBack" min="1" class="input-field py-1 text-sm" />
</div>
@@ -168,7 +170,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</div>
<span class="font-medium">Summary</span>
<span class="font-medium">{{ t('OlapConstructor.summaryCheckbox') }}</span>
</label>
<label class="flex items-center gap-2 px-2.5 h-[30px] rounded-md border cursor-pointer transition hover:bg-gray-50 text-xs"
@@ -180,7 +182,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg>
</div>
<span class="font-medium">Активно</span>
<span class="font-medium">{{ t('OlapConstructor.activeCheckbox') }}</span>
</label>
</div>
</div>
@@ -190,9 +192,9 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Рестораны -->
<div>
<label class="text-xs text-gray-500">Рестораны (можно несколько) *</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.restaurantsLabel') }}</label>
<button @click="openRestaurantModal" class="btn-secondary py-1.5 text-sm w-full flex justify-between items-center">
<span>{{ selectedRestaurants.length ? `Выбрано: ${selectedRestaurants.length}` : 'Выбрать рестораны' }}</span>
<span>{{ selectedRestaurants.length ? t('OlapConstructor.selectedCount', { count: selectedRestaurants.length }) : t('OlapConstructor.selectRestaurants') }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@@ -203,9 +205,9 @@
</div>
<!-- Подключение к БД -->
<div>
<label class="text-xs text-gray-500">Подключение к БД *</label>
<label class="text-xs text-gray-500">{{ t('OlapConstructor.dbConnectionLabel') }}</label>
<button @click="openDbConnectionModal" class="btn-secondary py-1.5 text-sm w-full flex justify-between items-center">
<span>{{ selectedDbConnection ? selectedDbConnection.name : 'Выбрать подключение' }}</span>
<span>{{ selectedDbConnection ? selectedDbConnection.name : t('OlapConstructor.selectDbConnection') }}</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
@@ -221,12 +223,12 @@
<button @click="activeTab = 'table'"
class="px-3 py-1.5 text-sm font-medium transition-colors"
:class="activeTab === 'table' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
Таблица
{{ t('OlapConstructor.tabTable') }}
</button>
<button @click="activeTab = 'sql'"
class="px-3 py-1.5 text-sm font-medium transition-colors"
:class="activeTab === 'sql' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
SQL скрипт
{{ t('OlapConstructor.tabSql') }}
</button>
</div>
<div class="p-2">
@@ -237,32 +239,32 @@
@dragleave="dragOverZone = null"
@drop="dropOnZone('filter', $event)"
:class="{ 'ring-2 ring-primary-400 bg-primary-50 rounded-lg': dragOverZone === 'filter' }">
<h3 class="font-bold text-gray-800 text-sm mb-1.5">Пользовательские фильтры</h3>
<h3 class="font-bold text-gray-800 text-sm mb-1.5">{{ t('OlapConstructor.userFiltersTitle') }}</h3>
<div class="flex flex-wrap gap-2">
<div v-for="(f, idx) in filterFields" :key="f.id"
class="inline-flex items-center gap-1.5 bg-cyan-100 text-cyan-800 px-2 py-1 rounded-full text-xs">
{{ f.name }}
<div class="filter-control inline-flex gap-1 ml-1">
<select v-model="f.filterType" class="text-xs bg-transparent border rounded px-1 py-0">
<option value="IncludeValues">IncludeValues</option>
<option value="ExcludeValues">ExcludeValues</option>
<option value="EnumValue">EnumValue</option>
<option value="StringValue">StringValue</option>
<option value="IncludeValues">{{ t('OlapConstructor.filterTypes.IncludeValues') }}</option>
<option value="ExcludeValues">{{ t('OlapConstructor.filterTypes.ExcludeValues') }}</option>
<option value="EnumValue">{{ t('OlapConstructor.filterTypes.EnumValue') }}</option>
<option value="StringValue">{{ t('OlapConstructor.filterTypes.StringValue') }}</option>
</select>
<template v-if="f.filterType === 'IncludeValues' || f.filterType === 'ExcludeValues'">
<input type="text" v-model="f.valuesString" placeholder="знач1,знач2" class="text-xs border rounded px-1 w-24 py-0" @input="parseValues(f)" />
<input type="text" v-model="f.valuesString" :placeholder="t('OlapConstructor.IncludeValuesPlaceholder')" class="text-xs border rounded px-1 w-24 py-0" @input="parseValues(f)" />
</template>
<template v-else-if="f.filterType === 'EnumValue'">
<input v-model="f.enumKey" placeholder="enumKey" class="text-xs border rounded px-1 w-16 py-0" />
<input v-model="f.enumValue" placeholder="значение" class="text-xs border rounded px-1 w-20 py-0" />
<input v-model="f.enumValue" :placeholder="t('OlapConstructor.valuePlaceholder')" class="text-xs border rounded px-1 w-20 py-0" />
</template>
<template v-else-if="f.filterType === 'StringValue'">
<input v-model="f.value" placeholder="значение" class="text-xs border rounded px-1 w-20 py-0" />
<input v-model="f.value" :placeholder="t('OlapConstructor.valuePlaceholder')" class="text-xs border rounded px-1 w-20 py-0" />
</template>
</div>
<button @click="removeFilter(idx)" class="text-cyan-600 hover:text-cyan-800 ml-0.5"></button>
</div>
<div v-if="!filterFields.length" class="text-gray-400 text-xs">Перетащите поле фильтра</div>
<div v-if="!filterFields.length" class="text-gray-400 text-xs">{{ t('OlapConstructor.dropFilterHint') }}</div>
</div>
</div>
@@ -277,7 +279,7 @@
@dragleave="dragOverZone = null"
@drop="dropOnZone('row', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'row' }">
<div class="text-xs font-bold text-gray-600">ROW</div>
<div class="text-xs font-bold text-gray-600">{{ t('OlapConstructor.rowHeader') }}</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in rowFields" :key="f.id"
class="inline-flex items-center gap-1 bg-pink-100 text-pink-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
@@ -290,7 +292,7 @@
{{ f.name }}
<button @click="removeRow(idx)" class="text-pink-600 hover:text-pink-800"></button>
</div>
<span v-if="!rowFields.length" class="text-gray-400 text-xs">Перетащите категорию</span>
<span v-if="!rowFields.length" class="text-gray-400 text-xs">{{ t('OlapConstructor.dropCategoryHint') }}</span>
</div>
</th>
<th class="bg-gray-100 p-1.5 text-center"
@@ -299,7 +301,7 @@
@dragleave="dragOverZone = null"
@drop="dropOnZone('column', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'column' }">
<div class="text-xs font-bold text-gray-600">COLUMN</div>
<div class="text-xs font-bold text-gray-600">{{ t('OlapConstructor.columnHeader') }}</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in columnFields" :key="f.id"
class="inline-flex items-center gap-1 bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
@@ -312,7 +314,7 @@
{{ f.name }}
<button @click="removeColumn(idx)" class="text-blue-600 hover:text-blue-800"></button>
</div>
<span v-if="!columnFields.length" class="text-gray-400 text-xs">Перетащите категорию</span>
<span v-if="!columnFields.length" class="text-gray-400 text-xs">{{ t('OlapConstructor.dropCategoryHint') }}</span>
</div>
</th>
</tr>
@@ -325,7 +327,7 @@
@dragleave="dragOverZone = null"
@drop="dropOnZone('value', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'value' }">
<div class="text-xs font-bold text-gray-600">VALUES</div>
<div class="text-xs font-bold text-gray-600">{{ t('OlapConstructor.valuesHeader') }}</div>
<div class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in valueFields" :key="f.id"
class="inline-flex items-center gap-1 bg-amber-100 text-amber-800 px-1.5 py-0.5 rounded-full text-xs cursor-move"
@@ -337,13 +339,13 @@
@drop="dropReorder($event, 'value', idx)">
{{ f.name }}
<select v-model="f.aggregation" class="text-xs bg-transparent border rounded px-0.5 py-0" @click.stop>
<option value="sum">SUM</option>
<option value="avg">AVG</option>
<option value="count">COUNT</option>
<option value="sum">{{ t('OlapConstructor.aggregations.sum') }}</option>
<option value="avg">{{ t('OlapConstructor.aggregations.avg') }}</option>
<option value="count">{{ t('OlapConstructor.aggregations.count') }}</option>
</select>
<button @click="removeValue(idx)" class="text-amber-600 hover:text-amber-800"></button>
</div>
<span v-if="!valueFields.length" class="text-gray-400 text-xs">Перетащите число</span>
<span v-if="!valueFields.length" class="text-gray-400 text-xs">{{ t('OlapConstructor.dropNumberHint') }}</span>
</div>
</th>
</tr>
@@ -361,8 +363,8 @@
<div v-if="activeTab === 'sql'">
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-gray-800 text-sm">Скрипт SQL</h3>
<button @click="copySQL" class="btn-secondary text-xs py-1 px-2">Копировать SQL</button>
<h3 class="font-bold text-gray-800 text-sm">{{ t('OlapConstructor.tabSql') }}</h3>
<button @click="copySQL" class="btn-secondary text-xs py-1 px-2">{{ t('OlapConstructor.copySql') }}</button>
</div>
<div class="bg-gray-900 text-gray-200 p-2 rounded-lg overflow-x-auto font-mono text-xs whitespace-pre-wrap">
<pre>{{ sqlScript }}</pre>
@@ -385,11 +387,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" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Сброс всех настроек</h3>
<p class="text-sm text-gray-500 mb-6">Вы уверены? Все выбранные поля, фильтры, настройки будут удалены.</p>
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('OlapConstructor.resetModalTitle') }}</h3>
<p class="text-sm text-gray-500 mb-6">{{ t('OlapConstructor.resetModalMessage') }}</p>
<div class="flex justify-center space-x-3">
<button @click="resetModal.show = false" class="btn-secondary">Отмена</button>
<button @click="confirmReset" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">Сбросить</button>
<button @click="resetModal.show = false" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmReset" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">{{ t('app.reset') }}</button>
</div>
</div>
</div>
@@ -404,7 +406,7 @@
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[90vh] flex flex-col">
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-xl font-bold text-gray-900">Выбор ресторанов</h3>
<h3 class="text-xl font-bold text-gray-900">{{ t('OlapConstructor.restaurantModalTitle') }}</h3>
<button @click="closeRestaurantModal" class="text-gray-400 hover:text-gray-600">
<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" />
@@ -416,7 +418,7 @@
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" 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" />
</svg>
<input v-model="restaurantModal.search" type="text" class="input-field pl-9" placeholder="Поиск по названию или хосту" />
<input v-model="restaurantModal.search" type="text" class="input-field pl-9" :placeholder="t('OlapConstructor.restaurantSearchPlaceholder')" />
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2">
@@ -441,11 +443,11 @@
</svg>
</div>
</div>
<div v-if="filteredRestaurantsList.length === 0" class="text-center py-8 text-gray-500">Рестораны не найдены</div>
<div v-if="filteredRestaurantsList.length === 0" class="text-center py-8 text-gray-500">{{ t('OlapConstructor.noRestaurantsFound') }}</div>
</div>
<div class="flex justify-end space-x-3 p-6 border-t bg-gray-50 rounded-b-2xl">
<button @click="closeRestaurantModal" class="btn-secondary">Отмена</button>
<button @click="confirmRestaurants" class="btn-primary">Подтвердить</button>
<button @click="closeRestaurantModal" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmRestaurants" class="btn-primary">{{ t('app.confirm') }}</button>
</div>
</div>
</div>
@@ -459,7 +461,7 @@
<div class="flex items-center justify-center min-h-screen p-4">
<div class="relative bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[90vh] flex flex-col">
<div class="flex justify-between items-center p-6 border-b">
<h3 class="text-xl font-bold text-gray-900">Подключение к БД</h3>
<h3 class="text-xl font-bold text-gray-900">{{ t('OlapConstructor.dbModalTitle') }}</h3>
<button @click="closeDbConnectionModal" class="text-gray-400 hover:text-gray-600">
<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" />
@@ -471,7 +473,7 @@
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" 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" />
</svg>
<input v-model="dbConnectionModal.search" type="text" class="input-field pl-9" placeholder="Поиск по имени" />
<input v-model="dbConnectionModal.search" type="text" class="input-field pl-9" :placeholder="t('OlapConstructor.dbSearchPlaceholder')" />
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2">
@@ -489,11 +491,11 @@
</svg>
</div>
</div>
<div v-if="filteredDbConnectionsList.length === 0" class="text-center py-8 text-gray-500">Подключения не найдены</div>
<div v-if="filteredDbConnectionsList.length === 0" class="text-center py-8 text-gray-500">{{ t('OlapConstructor.noConnectionsFound') }}</div>
</div>
<div class="flex justify-end space-x-3 p-6 border-t bg-gray-50 rounded-b-2xl">
<button @click="closeDbConnectionModal" class="btn-secondary">Отмена</button>
<button @click="confirmDbConnection" class="btn-primary">Подтвердить</button>
<button @click="closeDbConnectionModal" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmDbConnection" class="btn-primary">{{ t('app.confirm') }}</button>
</div>
</div>
</div>
@@ -512,11 +514,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" />
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">Несохранённые изменения</h3>
<p class="text-sm text-gray-500 mb-6">Вы уверены, что хотите выйти? Все несохранённые данные будут потеряны.</p>
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ t('OlapConstructor.exitModalTitle') }}</h3>
<p class="text-sm text-gray-500 mb-6">{{ t('OlapConstructor.exitModalMessage') }}</p>
<div class="flex justify-center space-x-3">
<button @click="exitConfirmModal.show = false" class="btn-secondary">Остаться</button>
<button @click="confirmExit" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">Выйти</button>
<button @click="exitConfirmModal.show = false" class="btn-secondary">{{ t('OlapConstructor.exitModalStay') }}</button>
<button @click="confirmExit" class="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700">{{ t('OlapConstructor.exitModalLeave') }}</button>
</div>
</div>
</div>
@@ -529,9 +531,11 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import AppLayout from '@/components/Layout/AppLayout.vue'
import { useNotification } from '@/composables/useNotification'
const { t } = useI18n()
const { showNotification } = useNotification()
const route = useRoute()
const router = useRouter()
@@ -624,7 +628,7 @@ const selectedRestaurants = ref<Restaurant[]>([])
const selectedDbConnection = ref<DbConnection | null>(null)
// SQL скрипт, генерируемый на бэкенде
const sqlScript = ref('-- Выберите подключение к БД для генерации SQL')
const sqlScript = ref(t('OlapConstructor.defaultSqlPlaceholder'))
let sqlDebounceTimer: any = null
// Модалка ресторанов
@@ -715,7 +719,7 @@ const fetchColumns = async () => {
refreshFieldsAndReset()
} catch (err: any) {
console.error(err)
error.value = `Ошибка загрузки полей: ${err.message}`
error.value = t('OlapConstructor.notifications.loadColumnsError', { error: err.message })
showNotification(error.value, 'error')
columnsData.value = []
refreshFieldsAndReset()
@@ -731,7 +735,7 @@ const loadRestaurants = async () => {
if (!res.ok) throw new Error()
restaurantsList.value = await res.json()
} catch (err) {
showNotification('Не удалось загрузить список ресторанов', 'error')
showNotification('OlapConstructor.notifications.errorLoadRestaurants', 'error')
}
}
@@ -741,14 +745,14 @@ const loadDbConnections = async () => {
if (!res.ok) throw new Error()
dbConnectionsList.value = await res.json()
} catch (err) {
showNotification('Не удалось загрузить список подключений к БД', 'error')
showNotification('OlapConstructor.notifications.errorLoadDB', 'error')
}
}
// Генерация SQL через API (для вкладки SQL)
async function fetchSql() {
if (!selectedDbConnection.value) {
sqlScript.value = '-- Выберите подключение к БД для генерации SQL'
sqlScript.value = t('OlapConstructor.notifications.dbConnectionRequired')
return
}
const config = buildConfigObject()
@@ -761,7 +765,7 @@ async function fetchSql() {
if (!res.ok) throw new Error(await res.text())
sqlScript.value = await res.text()
} catch (e: any) {
sqlScript.value = `-- Ошибка генерации SQL: ${e.message}`
sqlScript.value = t('OlapConstructor.notifications.sqlGenerationError', { error: e.message })
}
}
@@ -834,9 +838,9 @@ async function exportConfigAsJson() {
a.download = `iiko_olap_${reportType.value.toLowerCase()}.json`;
a.click();
URL.revokeObjectURL(url);
showNotification('Конфигурация iiko экспортирована', 'success');
showNotification('OlapConstructor.notifications.exportSuccess', 'success');
} catch (e: any) {
showNotification('Ошибка экспорта: ' + e.message, 'error');
showNotification('OlapConstructor.notifications.exportError', 'error', { error: e.message });
}
}
@@ -912,9 +916,9 @@ function importConfigFromJson(event: Event) {
})
}
isDirty.value = true
showNotification('Конфигурация iiko загружена', 'success')
showNotification('OlapConstructor.notifications.exportSuccess', 'success');
} catch (err: any) {
showNotification('Ошибка при загрузке JSON: ' + err.message, 'error')
showNotification('OlapConstructor.notifications.importError', 'error', { error: err.message });
}
input.value = ''
}
@@ -1016,7 +1020,7 @@ async function loadQuery(id: number) {
triggerSqlUpdate()
isDirty.value = false
} catch (e: any) {
showNotification('Ошибка загрузки запроса: ' + e.message, 'error')
showNotification('OlapConstructor.notifications.loadQueryError', 'error', { error: e.message });
} finally {
isLoadingQuery.value = false
}
@@ -1024,19 +1028,19 @@ async function loadQuery(id: number) {
async function saveQuery() {
if (!queryName.value.trim()) {
showNotification('Введите имя запроса', 'error')
showNotification('OlapConstructor.notifications.queryNameRequired', 'error');
return
}
if (!selectedDbConnection.value) {
showNotification('Выберите подключение к БД', 'error')
showNotification('OlapConstructor.notifications.dbConnectionRequired', 'error');
return
}
if (selectedRestaurants.value.length === 0) {
showNotification('Выберите хотя бы один ресторан', 'error')
showNotification('OlapConstructor.notifications.restaurantsRequired', 'error');
return
}
if (!tableName.value.trim()) {
showNotification('Укажите название таблицы SQL', 'error');
showNotification('OlapConstructor.notifications.tableNameRequired', 'error');
return;
}
const config = buildConfigObject()
@@ -1056,10 +1060,10 @@ async function saveQuery() {
body: JSON.stringify(payload)
})
if (!res.ok) throw new Error(await res.text())
showNotification('Запрос сохранён', 'success')
showNotification('OlapConstructor.notifications.saveSuccess', 'success')
isDirty.value = false
} catch (e: any) {
showNotification('Ошибка сохранения: ' + e.message, 'error')
showNotification('OlapConstructor.notifications.saveError', 'error', { error: e.message })
}
}
@@ -1267,7 +1271,7 @@ const confirmReset = () => {
queryName.value = ''
selectedRestaurants.value = []
selectedDbConnection.value = null
sqlScript.value = '-- Выберите подключение к БД для генерации SQL'
sqlScript.value = t('OlapConstructor.notifications.dbConnectionRequired')
isDirty.value = true
showNotification('Все настройки сброшены', 'success')
}
@@ -1276,7 +1280,7 @@ const toggleSection = (section: 'number' | 'category' | 'filter') => {
}
const copySQL = () => {
navigator.clipboard.writeText(sqlScript.value)
showNotification('SQL скрипт скопирован', 'success')
showNotification('OlapConstructor.notifications.sqlCopied', 'success')
}
// Модалка ресторанов
const openRestaurantModal = async () => {
@@ -1296,7 +1300,7 @@ const confirmRestaurants = () => {
selectedRestaurants.value = [...tempSelectedRestaurants.value]
closeRestaurantModal()
triggerSqlUpdate()
showNotification('Рестораны выбраны', 'success')
showNotification('OlapConstructor.notifications.restaurantsSelected', 'success')
}
// Модалка БД
const openDbConnectionModal = async () => {
@@ -1311,7 +1315,7 @@ const confirmDbConnection = () => {
selectedDbConnection.value = tempSelectedDbConnection.value ? { ...tempSelectedDbConnection.value } : null
closeDbConnectionModal()
triggerSqlUpdate()
showNotification('Подключение к БД выбрано', 'success')
showNotification('OlapConstructor.notifications.dbConnectionSelected', 'success')
}
// Фильтрация для модалок
const filteredRestaurantsList = computed(() => {

View File

@@ -16,7 +16,7 @@
<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">{{ t('olapQueries.result') }}</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">{{ t('common.restaurants') }}</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('app.restaurants') }}</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">{{ t('common.actions') }}</th>
</tr>