@@ -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 ( ( ) => {