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

@@ -246,6 +246,97 @@
"deleteSuccess": "Query deleted", "deleteSuccess": "Query deleted",
"deleteError": "Delete error" "deleteError": "Delete error"
}, },
"OlapConstructor": {
"titleNew": "New OLAP Query",
"titleEdit": "Edit Query",
"saveQuery": "Save Query",
"exportJson": "Export JSON",
"importJson": "Import JSON",
"fieldsTitle": "Fields",
"searchPlaceholder": "Search by name or tags",
"foundCount": "Found: {found} / {total}",
"loadingFields": "Loading fields...",
"noFields": "No fields. Initialize the structure in the OLAP Columns section first.",
"numericGroup": "Numeric → VALUES",
"categoryGroup": "Categories → ROW / COLUMN",
"filtersGroup": "Filters",
"resetAll": "Reset All",
"queryNameLabel": "Query name *",
"queryNamePlaceholder": "For example: Sales by day",
"valuePlaceholder": "value",
"IncludeValuesPlaceholder": "value1,value2",
"reportTypeLabel": "Report type",
"sqlTableLabel": "SQL table *",
"dateToLabel": "Date to (end of day)",
"daysBackLabel": "Days back (≥1)",
"summaryCheckbox": "Summary",
"activeCheckbox": "Active",
"restaurantsLabel": "Restaurants (multiple) *",
"selectRestaurants": "Select restaurants",
"selectedCount": "Selected: {count}",
"dbConnectionLabel": "Database connection *",
"selectDbConnection": "Select connection",
"tabTable": "Table",
"tabSql": "SQL script",
"copySql": "Copy SQL",
"defaultSqlPlaceholder": "-- Select a database connection to generate SQL",
"userFiltersTitle": "Custom filters",
"dropFilterHint": "Drop a filter field",
"rowHeader": "ROW",
"columnHeader": "COLUMN",
"valuesHeader": "VALUES",
"dropCategoryHint": "Drop a category",
"dropNumberHint": "Drop a number",
"resetModalTitle": "Reset All Settings",
"resetModalMessage": "Are you sure? All selected fields, filters, and settings will be deleted.",
"restaurantModalTitle": "Select Restaurants",
"restaurantSearchPlaceholder": "Search by name or host",
"noRestaurantsFound": "No restaurants found",
"dbModalTitle": "Database Connection",
"dbSearchPlaceholder": "Search by name",
"noConnectionsFound": "No connections found",
"exitModalTitle": "Unsaved Changes",
"exitModalMessage": "Are you sure you want to exit? All unsaved data will be lost.",
"exitModalStay": "Stay",
"exitModalLeave": "Leave",
"reportTypes": {
"SALES": "SALES",
"DELIVERIES": "DELIVERIES",
"TRANSACTIONS": "TRANSACTIONS"
},
"aggregations": {
"sum": "SUM",
"avg": "AVG",
"count": "COUNT"
},
"filterTypes": {
"IncludeValues": "Include Values",
"ExcludeValues": "Exclude Values",
"EnumValue": "Enum Value",
"StringValue": "String Value"
},
"notifications": {
"errorLoadRestaurants": "Couldn't load restaurant list",
"errorLoadDB": "The list of database connections could not be loaded",
"sqlGenerationError": "-- SQL generation error: {error}",
"exportSuccess": "iiko configuration exported",
"exportError": "Export error: {error}",
"importSuccess": "iiko configuration loaded",
"importError": "Error loading JSON: {error}",
"queryNameRequired": "Enter query name",
"dbConnectionRequired": "Select a database connection",
"restaurantsRequired": "Select at least one restaurant",
"tableNameRequired": "Specify SQL table name",
"saveSuccess": "Query saved",
"saveError": "Save error: {error}",
"loadQueryError": "Error loading query: {error}",
"resetSuccess": "All settings reset",
"sqlCopied": "SQL script copied",
"restaurantsSelected": "Restaurants selected",
"dbConnectionSelected": "Database connection selected",
"loadColumnsError": "Error loading fields: {error}"
}
},
"dbConnections": { "dbConnections": {
"pageName": "Databases", "pageName": "Databases",
"add": "Add Connection", "add": "Add Connection",

View File

@@ -246,6 +246,97 @@
"deleteSuccess": "Запрос удалён", "deleteSuccess": "Запрос удалён",
"deleteError": "Ошибка удаления" "deleteError": "Ошибка удаления"
}, },
"OlapConstructor": {
"titleNew": "Новый OLAP запрос",
"titleEdit": "Редактирование запроса",
"saveQuery": "Сохранить запрос",
"exportJson": "Экспорт JSON",
"importJson": "Импорт JSON",
"fieldsTitle": "Поля",
"searchPlaceholder": "Поиск по названию или тегам",
"foundCount": "Найдено: {found} / {total}",
"loadingFields": "Загрузка полей...",
"noFields": "Нет полей. Сначала инициализируйте структуру в разделе OLAP Columns.",
"numericGroup": "Числовые → VALUES",
"categoryGroup": "Категории → ROW / COLUMN",
"filtersGroup": "Фильтры",
"resetAll": "Сбросить всё",
"queryNameLabel": "Имя запроса *",
"queryNamePlaceholder": "Например: Продажи по дням",
"valuePlaceholder": "значение",
"IncludeValuesPlaceholder": "знач1,знач2",
"reportTypeLabel": "Тип отчета",
"sqlTableLabel": "Таблица SQL *",
"dateToLabel": "Дата до (конец дня)",
"daysBackLabel": "Дней назад (≥1)",
"summaryCheckbox": "Summary",
"activeCheckbox": "Активно",
"restaurantsLabel": "Рестораны (можно несколько) *",
"selectRestaurants": "Выбрать рестораны",
"selectedCount": "Выбрано: {count}",
"dbConnectionLabel": "Подключение к БД *",
"selectDbConnection": "Выбрать подключение",
"tabTable": "Таблица",
"tabSql": "SQL скрипт",
"copySql": "Копировать SQL",
"defaultSqlPlaceholder": "-- Выберите подключение к БД для генерации SQL",
"userFiltersTitle": "Пользовательские фильтры",
"dropFilterHint": "Перетащите поле фильтра",
"rowHeader": "ROW",
"columnHeader": "COLUMN",
"valuesHeader": "VALUES",
"dropCategoryHint": "Перетащите категорию",
"dropNumberHint": "Перетащите число",
"resetModalTitle": "Сброс всех настроек",
"resetModalMessage": "Вы уверены? Все выбранные поля, фильтры, настройки будут удалены.",
"restaurantModalTitle": "Выбор ресторанов",
"restaurantSearchPlaceholder": "Поиск по названию или хосту",
"noRestaurantsFound": "Рестораны не найдены",
"dbModalTitle": "Подключение к БД",
"dbSearchPlaceholder": "Поиск по имени",
"noConnectionsFound": "Подключения не найдены",
"exitModalTitle": "Несохранённые изменения",
"exitModalMessage": "Вы уверены, что хотите выйти? Все несохранённые данные будут потеряны.",
"exitModalStay": "Остаться",
"exitModalLeave": "Выйти",
"reportTypes": {
"SALES": "ПРОДАЖИ",
"DELIVERIES": "ДОСТАВКИ",
"TRANSACTIONS": "ТРАНЗАКЦИИ"
},
"aggregations": {
"sum": "СУММА",
"avg": "СРЕДНЕЕ",
"count": "КОЛИЧЕСТВО"
},
"filterTypes": {
"IncludeValues": "Включая значения",
"ExcludeValues": "Исключая значения",
"EnumValue": "Значение перечисления",
"StringValue": "Строковое значение"
},
"notifications": {
"errorLoadRestaurants": "Не удалось загрузить список ресторанов",
"errorLoadDB": "Не удалось загрузить список подключений к БД",
"sqlGenerationError": "-- Ошибка генерации SQL: {error}",
"exportSuccess": "Конфигурация iiko экспортирована",
"exportError": "Ошибка экспорта: {error}",
"importSuccess": "Конфигурация iiko загружена",
"importError": "Ошибка при загрузке JSON: {error}",
"queryNameRequired": "Введите имя запроса",
"dbConnectionRequired": "-- Выберите подключение к БД для генерации SQL",
"restaurantsRequired": "Выберите хотя бы один ресторан",
"tableNameRequired": "Укажите название таблицы SQL",
"saveSuccess": "Запрос сохранён",
"saveError": "Ошибка сохранения: {error}",
"loadQueryError": "Ошибка загрузки запроса: {error}",
"resetSuccess": "Все настройки сброшены",
"sqlCopied": "SQL скрипт скопирован",
"restaurantsSelected": "Рестораны выбраны",
"dbConnectionSelected": "Подключение к БД выбрано",
"loadColumnsError": "Ошибка загрузки полей: {error}"
}
},
"dbConnections": { "dbConnections": {
"pageName": "Базы данных", "pageName": "Базы данных",
"add": "Добавить подключение", "add": "Добавить подключение",

View File

@@ -2,26 +2,26 @@
<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"> <h1 class="text-2xl font-bold text-gray-900">
{{ queryId ? 'Редактирование запроса' : 'Новый OLAP запрос' }} {{ queryId ? t('OlapConstructor.titleEdit') : t('OlapConstructor.titleNew') }}
</h1> </h1>
<div class="flex gap-3"> <div class="flex gap-3">
<button @click="saveQuery" class="btn-primary flex items-center gap-2"> <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"> <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" /> <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> </svg>
Сохранить запрос {{ t('OlapConstructor.saveQuery') }}
</button> </button>
<button @click="exportConfigAsJson" class="btn-secondary flex items-center gap-2"> <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"> <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" /> <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> </svg>
Экспорт JSON {{ t('OlapConstructor.exportJson') }}
</button> </button>
<label class="btn-secondary flex items-center gap-2 cursor-pointer"> <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"> <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" /> <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> </svg>
Импорт JSON {{ t('OlapConstructor.importJson') }}
<input type="file" accept=".json" @change="importConfigFromJson" class="hidden"> <input type="file" accept=".json" @change="importConfigFromJson" class="hidden">
</label> </label>
<button @click="openExitModal" class="btn-danger flex items-center gap-2"> <button @click="openExitModal" class="btn-danger flex items-center gap-2">
@@ -35,20 +35,22 @@
<div class="flex flex-row gap-6"> <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)]"> <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"> <div class="mb-2">
<input type="text" v-model="searchQuery" placeholder="Поиск по названию или тегам" class="input-field py-1.5 text-sm"> <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">Найдено: {{ filteredAvailableFields.length }} / {{ availableFields.length }}</div> <div class="text-xs text-gray-500 mt-1" v-if="searchQuery">
{{ t('OlapConstructor.foundCount', { found: filteredAvailableFields.length, total: availableFields.length }) }}
</div>
</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="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 v-else class="flex-1 overflow-y-auto space-y-3 pr-1">
<!-- Числовые --> <!-- Числовые -->
<div> <div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('number')"> <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"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -70,7 +72,7 @@
<!-- Категории --> <!-- Категории -->
<div> <div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('category')"> <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"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -92,7 +94,7 @@
<!-- Фильтры --> <!-- Фильтры -->
<div> <div>
<div class="flex justify-between items-center cursor-pointer select-none" @click="toggleSection('filter')"> <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"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
@@ -113,7 +115,7 @@
</div> </div>
<div class="mt-3"> <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>
</div> </div>
@@ -122,38 +124,38 @@
<!-- Имя запроса отдельной строкой --> <!-- Имя запроса отдельной строкой -->
<div class="mb-4"> <div class="mb-4">
<label class="text-sm font-medium text-gray-700">Имя запроса *</label> <label class="text-sm font-medium text-gray-700">{{ t('OlapConstructor.queryNameLabel') }}</label>
<input type="text" v-model="queryName" placeholder="Например: Продажи по дням" class="input-field w-full" /> <input type="text" v-model="queryName" :placeholder="t('OlapConstructor.queryNamePlaceholder')" class="input-field w-full" />
</div> </div>
<div class="card p-3 mb-4"> <div class="card p-3 mb-4">
<div class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end"> <div class="grid grid-cols-1 md:grid-cols-6 gap-3 items-end">
<!-- Тип отчета --> <!-- Тип отчета -->
<div class="flex flex-col"> <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"> <select v-model="reportType" class="input-field py-1 text-sm">
<option value="SALES">SALES</option> <option value="SALES">{{ t('OlapConstructor.reportTypes.SALES') }}</option>
<option value="DELIVERIES">DELIVERIES</option> <option value="DELIVERIES">{{ t('OlapConstructor.reportTypes.DELIVERIES') }}</option>
<option value="TRANSACTIONS">TRANSACTIONS</option> <option value="TRANSACTIONS">{{ t('OlapConstructor.reportTypes.TRANSACTIONS') }}</option>
</select> </select>
</div> </div>
<!-- Таблица SQL --> <!-- Таблица SQL -->
<div class="flex flex-col"> <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" <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 }" /> :class="{ 'border-green-500 bg-green-50': tableNameValid && tableName, 'border-red-500 bg-red-50': tableNameTouched && !tableNameValid && tableName }" />
</div> </div>
<!-- Дата до --> <!-- Дата до -->
<div class="flex flex-col"> <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" /> <input type="date" v-model="dateTo" class="input-field py-1 text-sm" />
</div> </div>
<!-- Дней назад --> <!-- Дней назад -->
<div class="flex flex-col"> <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" /> <input type="number" v-model.number="daysBack" min="1" class="input-field py-1 text-sm" />
</div> </div>
@@ -168,7 +170,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<span class="font-medium">Summary</span> <span class="font-medium">{{ t('OlapConstructor.summaryCheckbox') }}</span>
</label> </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" <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
</svg> </svg>
</div> </div>
<span class="font-medium">Активно</span> <span class="font-medium">{{ t('OlapConstructor.activeCheckbox') }}</span>
</label> </label>
</div> </div>
</div> </div>
@@ -190,9 +192,9 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Рестораны --> <!-- Рестораны -->
<div> <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"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
@@ -203,9 +205,9 @@
</div> </div>
<!-- Подключение к БД --> <!-- Подключение к БД -->
<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"> <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"> <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" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
@@ -221,12 +223,12 @@
<button @click="activeTab = 'table'" <button @click="activeTab = 'table'"
class="px-3 py-1.5 text-sm font-medium transition-colors" 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'"> :class="activeTab === 'table' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
Таблица {{ t('OlapConstructor.tabTable') }}
</button> </button>
<button @click="activeTab = 'sql'" <button @click="activeTab = 'sql'"
class="px-3 py-1.5 text-sm font-medium transition-colors" 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'"> :class="activeTab === 'sql' ? 'text-primary-600 border-b-2 border-primary-600' : 'text-gray-500 hover:text-gray-700'">
SQL скрипт {{ t('OlapConstructor.tabSql') }}
</button> </button>
</div> </div>
<div class="p-2"> <div class="p-2">
@@ -237,32 +239,32 @@
@dragleave="dragOverZone = null" @dragleave="dragOverZone = null"
@drop="dropOnZone('filter', $event)" @drop="dropOnZone('filter', $event)"
:class="{ 'ring-2 ring-primary-400 bg-primary-50 rounded-lg': dragOverZone === 'filter' }"> :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 class="flex flex-wrap gap-2">
<div v-for="(f, idx) in filterFields" :key="f.id" <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"> class="inline-flex items-center gap-1.5 bg-cyan-100 text-cyan-800 px-2 py-1 rounded-full text-xs">
{{ f.name }} {{ f.name }}
<div class="filter-control inline-flex gap-1 ml-1"> <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"> <select v-model="f.filterType" class="text-xs bg-transparent border rounded px-1 py-0">
<option value="IncludeValues">IncludeValues</option> <option value="IncludeValues">{{ t('OlapConstructor.filterTypes.IncludeValues') }}</option>
<option value="ExcludeValues">ExcludeValues</option> <option value="ExcludeValues">{{ t('OlapConstructor.filterTypes.ExcludeValues') }}</option>
<option value="EnumValue">EnumValue</option> <option value="EnumValue">{{ t('OlapConstructor.filterTypes.EnumValue') }}</option>
<option value="StringValue">StringValue</option> <option value="StringValue">{{ t('OlapConstructor.filterTypes.StringValue') }}</option>
</select> </select>
<template v-if="f.filterType === 'IncludeValues' || f.filterType === 'ExcludeValues'"> <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>
<template v-else-if="f.filterType === 'EnumValue'"> <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.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>
<template v-else-if="f.filterType === 'StringValue'"> <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> </template>
</div> </div>
<button @click="removeFilter(idx)" class="text-cyan-600 hover:text-cyan-800 ml-0.5"></button> <button @click="removeFilter(idx)" class="text-cyan-600 hover:text-cyan-800 ml-0.5"></button>
</div> </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>
</div> </div>
@@ -277,7 +279,7 @@
@dragleave="dragOverZone = null" @dragleave="dragOverZone = null"
@drop="dropOnZone('row', $event)" @drop="dropOnZone('row', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'row' }"> :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 class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in rowFields" :key="f.id" <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" 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 }} {{ f.name }}
<button @click="removeRow(idx)" class="text-pink-600 hover:text-pink-800"></button> <button @click="removeRow(idx)" class="text-pink-600 hover:text-pink-800"></button>
</div> </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> </div>
</th> </th>
<th class="bg-gray-100 p-1.5 text-center" <th class="bg-gray-100 p-1.5 text-center"
@@ -299,7 +301,7 @@
@dragleave="dragOverZone = null" @dragleave="dragOverZone = null"
@drop="dropOnZone('column', $event)" @drop="dropOnZone('column', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'column' }"> :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 class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in columnFields" :key="f.id" <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" 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 }} {{ f.name }}
<button @click="removeColumn(idx)" class="text-blue-600 hover:text-blue-800"></button> <button @click="removeColumn(idx)" class="text-blue-600 hover:text-blue-800"></button>
</div> </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> </div>
</th> </th>
</tr> </tr>
@@ -325,7 +327,7 @@
@dragleave="dragOverZone = null" @dragleave="dragOverZone = null"
@drop="dropOnZone('value', $event)" @drop="dropOnZone('value', $event)"
:class="{ 'bg-primary-50': dragOverZone === 'value' }"> :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 class="flex flex-wrap justify-center gap-1 mt-1">
<div v-for="(f, idx) in valueFields" :key="f.id" <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" 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)"> @drop="dropReorder($event, 'value', idx)">
{{ f.name }} {{ f.name }}
<select v-model="f.aggregation" class="text-xs bg-transparent border rounded px-0.5 py-0" @click.stop> <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="sum">{{ t('OlapConstructor.aggregations.sum') }}</option>
<option value="avg">AVG</option> <option value="avg">{{ t('OlapConstructor.aggregations.avg') }}</option>
<option value="count">COUNT</option> <option value="count">{{ t('OlapConstructor.aggregations.count') }}</option>
</select> </select>
<button @click="removeValue(idx)" class="text-amber-600 hover:text-amber-800"></button> <button @click="removeValue(idx)" class="text-amber-600 hover:text-amber-800"></button>
</div> </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> </div>
</th> </th>
</tr> </tr>
@@ -361,8 +363,8 @@
<div v-if="activeTab === 'sql'"> <div v-if="activeTab === 'sql'">
<div class="flex justify-between items-center mb-2"> <div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-gray-800 text-sm">Скрипт SQL</h3> <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">Копировать SQL</button> <button @click="copySQL" class="btn-secondary text-xs py-1 px-2">{{ t('OlapConstructor.copySql') }}</button>
</div> </div>
<div class="bg-gray-900 text-gray-200 p-2 rounded-lg overflow-x-auto font-mono text-xs whitespace-pre-wrap"> <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> <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" /> <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('OlapConstructor.resetModalTitle') }}</h3>
<p class="text-sm text-gray-500 mb-6">Вы уверены? Все выбранные поля, фильтры, настройки будут удалены.</p> <p class="text-sm text-gray-500 mb-6">{{ t('OlapConstructor.resetModalMessage') }}</p>
<div class="flex justify-center space-x-3"> <div class="flex justify-center space-x-3">
<button @click="resetModal.show = false" class="btn-secondary">Отмена</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">Сбросить</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> </div>
</div> </div>
@@ -404,7 +406,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-lg w-full max-h-[90vh] flex flex-col"> <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"> <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"> <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"> <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" />
@@ -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"> <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" /> <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> </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> </div>
<div class="flex-1 overflow-y-auto p-2 space-y-2"> <div class="flex-1 overflow-y-auto p-2 space-y-2">
@@ -441,11 +443,11 @@
</svg> </svg>
</div> </div>
</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>
<div class="flex justify-end space-x-3 p-6 border-t bg-gray-50 rounded-b-2xl"> <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="closeRestaurantModal" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmRestaurants" class="btn-primary">Подтвердить</button> <button @click="confirmRestaurants" class="btn-primary">{{ t('app.confirm') }}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -459,7 +461,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-lg w-full max-h-[90vh] flex flex-col"> <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"> <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"> <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"> <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" />
@@ -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"> <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" /> <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> </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> </div>
<div class="flex-1 overflow-y-auto p-2 space-y-2"> <div class="flex-1 overflow-y-auto p-2 space-y-2">
@@ -489,11 +491,11 @@
</svg> </svg>
</div> </div>
</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>
<div class="flex justify-end space-x-3 p-6 border-t bg-gray-50 rounded-b-2xl"> <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="closeDbConnectionModal" class="btn-secondary">{{ t('app.cancel') }}</button>
<button @click="confirmDbConnection" class="btn-primary">Подтвердить</button> <button @click="confirmDbConnection" class="btn-primary">{{ t('app.confirm') }}</button>
</div> </div>
</div> </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" /> <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('OlapConstructor.exitModalTitle') }}</h3>
<p class="text-sm text-gray-500 mb-6">Вы уверены, что хотите выйти? Все несохранённые данные будут потеряны.</p> <p class="text-sm text-gray-500 mb-6">{{ t('OlapConstructor.exitModalMessage') }}</p>
<div class="flex justify-center space-x-3"> <div class="flex justify-center space-x-3">
<button @click="exitConfirmModal.show = false" class="btn-secondary">Остаться</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">Выйти</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> </div>
</div> </div>
@@ -529,9 +531,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue' import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
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 route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -624,7 +628,7 @@ const selectedRestaurants = ref<Restaurant[]>([])
const selectedDbConnection = ref<DbConnection | null>(null) const selectedDbConnection = ref<DbConnection | null>(null)
// SQL скрипт, генерируемый на бэкенде // SQL скрипт, генерируемый на бэкенде
const sqlScript = ref('-- Выберите подключение к БД для генерации SQL') const sqlScript = ref(t('OlapConstructor.defaultSqlPlaceholder'))
let sqlDebounceTimer: any = null let sqlDebounceTimer: any = null
// Модалка ресторанов // Модалка ресторанов
@@ -715,7 +719,7 @@ const fetchColumns = async () => {
refreshFieldsAndReset() refreshFieldsAndReset()
} catch (err: any) { } catch (err: any) {
console.error(err) console.error(err)
error.value = `Ошибка загрузки полей: ${err.message}` error.value = t('OlapConstructor.notifications.loadColumnsError', { error: err.message })
showNotification(error.value, 'error') showNotification(error.value, 'error')
columnsData.value = [] columnsData.value = []
refreshFieldsAndReset() refreshFieldsAndReset()
@@ -731,7 +735,7 @@ const loadRestaurants = async () => {
if (!res.ok) throw new Error() if (!res.ok) throw new Error()
restaurantsList.value = await res.json() restaurantsList.value = await res.json()
} catch (err) { } catch (err) {
showNotification('Не удалось загрузить список ресторанов', 'error') showNotification('OlapConstructor.notifications.errorLoadRestaurants', 'error')
} }
} }
@@ -741,14 +745,14 @@ const loadDbConnections = async () => {
if (!res.ok) throw new Error() if (!res.ok) throw new Error()
dbConnectionsList.value = await res.json() dbConnectionsList.value = await res.json()
} catch (err) { } catch (err) {
showNotification('Не удалось загрузить список подключений к БД', 'error') showNotification('OlapConstructor.notifications.errorLoadDB', 'error')
} }
} }
// Генерация SQL через API (для вкладки SQL) // Генерация SQL через API (для вкладки SQL)
async function fetchSql() { async function fetchSql() {
if (!selectedDbConnection.value) { if (!selectedDbConnection.value) {
sqlScript.value = '-- Выберите подключение к БД для генерации SQL' sqlScript.value = t('OlapConstructor.notifications.dbConnectionRequired')
return return
} }
const config = buildConfigObject() const config = buildConfigObject()
@@ -761,7 +765,7 @@ async function fetchSql() {
if (!res.ok) throw new Error(await res.text()) if (!res.ok) throw new Error(await res.text())
sqlScript.value = await res.text() sqlScript.value = await res.text()
} catch (e: any) { } 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.download = `iiko_olap_${reportType.value.toLowerCase()}.json`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification('Конфигурация iiko экспортирована', 'success'); showNotification('OlapConstructor.notifications.exportSuccess', 'success');
} catch (e: any) { } 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 isDirty.value = true
showNotification('Конфигурация iiko загружена', 'success') showNotification('OlapConstructor.notifications.exportSuccess', 'success');
} catch (err: any) { } catch (err: any) {
showNotification('Ошибка при загрузке JSON: ' + err.message, 'error') showNotification('OlapConstructor.notifications.importError', 'error', { error: err.message });
} }
input.value = '' input.value = ''
} }
@@ -1016,7 +1020,7 @@ async function loadQuery(id: number) {
triggerSqlUpdate() triggerSqlUpdate()
isDirty.value = false isDirty.value = false
} catch (e: any) { } catch (e: any) {
showNotification('Ошибка загрузки запроса: ' + e.message, 'error') showNotification('OlapConstructor.notifications.loadQueryError', 'error', { error: e.message });
} finally { } finally {
isLoadingQuery.value = false isLoadingQuery.value = false
} }
@@ -1024,19 +1028,19 @@ async function loadQuery(id: number) {
async function saveQuery() { async function saveQuery() {
if (!queryName.value.trim()) { if (!queryName.value.trim()) {
showNotification('Введите имя запроса', 'error') showNotification('OlapConstructor.notifications.queryNameRequired', 'error');
return return
} }
if (!selectedDbConnection.value) { if (!selectedDbConnection.value) {
showNotification('Выберите подключение к БД', 'error') showNotification('OlapConstructor.notifications.dbConnectionRequired', 'error');
return return
} }
if (selectedRestaurants.value.length === 0) { if (selectedRestaurants.value.length === 0) {
showNotification('Выберите хотя бы один ресторан', 'error') showNotification('OlapConstructor.notifications.restaurantsRequired', 'error');
return return
} }
if (!tableName.value.trim()) { if (!tableName.value.trim()) {
showNotification('Укажите название таблицы SQL', 'error'); showNotification('OlapConstructor.notifications.tableNameRequired', 'error');
return; return;
} }
const config = buildConfigObject() const config = buildConfigObject()
@@ -1056,10 +1060,10 @@ async function saveQuery() {
body: JSON.stringify(payload) body: JSON.stringify(payload)
}) })
if (!res.ok) throw new Error(await res.text()) if (!res.ok) throw new Error(await res.text())
showNotification('Запрос сохранён', 'success') showNotification('OlapConstructor.notifications.saveSuccess', 'success')
isDirty.value = false isDirty.value = false
} catch (e: any) { } catch (e: any) {
showNotification('Ошибка сохранения: ' + e.message, 'error') showNotification('OlapConstructor.notifications.saveError', 'error', { error: e.message })
} }
} }
@@ -1267,7 +1271,7 @@ const confirmReset = () => {
queryName.value = '' queryName.value = ''
selectedRestaurants.value = [] selectedRestaurants.value = []
selectedDbConnection.value = null selectedDbConnection.value = null
sqlScript.value = '-- Выберите подключение к БД для генерации SQL' sqlScript.value = t('OlapConstructor.notifications.dbConnectionRequired')
isDirty.value = true isDirty.value = true
showNotification('Все настройки сброшены', 'success') showNotification('Все настройки сброшены', 'success')
} }
@@ -1276,7 +1280,7 @@ const toggleSection = (section: 'number' | 'category' | 'filter') => {
} }
const copySQL = () => { const copySQL = () => {
navigator.clipboard.writeText(sqlScript.value) navigator.clipboard.writeText(sqlScript.value)
showNotification('SQL скрипт скопирован', 'success') showNotification('OlapConstructor.notifications.sqlCopied', 'success')
} }
// Модалка ресторанов // Модалка ресторанов
const openRestaurantModal = async () => { const openRestaurantModal = async () => {
@@ -1296,7 +1300,7 @@ const confirmRestaurants = () => {
selectedRestaurants.value = [...tempSelectedRestaurants.value] selectedRestaurants.value = [...tempSelectedRestaurants.value]
closeRestaurantModal() closeRestaurantModal()
triggerSqlUpdate() triggerSqlUpdate()
showNotification('Рестораны выбраны', 'success') showNotification('OlapConstructor.notifications.restaurantsSelected', 'success')
} }
// Модалка БД // Модалка БД
const openDbConnectionModal = async () => { const openDbConnectionModal = async () => {
@@ -1311,7 +1315,7 @@ const confirmDbConnection = () => {
selectedDbConnection.value = tempSelectedDbConnection.value ? { ...tempSelectedDbConnection.value } : null selectedDbConnection.value = tempSelectedDbConnection.value ? { ...tempSelectedDbConnection.value } : null
closeDbConnectionModal() closeDbConnectionModal()
triggerSqlUpdate() triggerSqlUpdate()
showNotification('Подключение к БД выбрано', 'success') showNotification('OlapConstructor.notifications.dbConnectionSelected', 'success')
} }
// Фильтрация для модалок // Фильтрация для модалок
const filteredRestaurantsList = computed(() => { 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.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.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('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-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> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">{{ t('common.actions') }}</th>
</tr> </tr>