up OLAPConstructor.vue

This commit is contained in:
2026-05-07 15:45:47 +03:00
parent 4e60a78fbd
commit c108ad4a5a

View File

@@ -106,101 +106,107 @@
<!-- Правая часть -->
<div class="flex-1 overflow-x-auto">
<div class="card p-3 mb-4 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 items-end">
<div class="card p-3 mb-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3 items-end">
<!-- Тип отчета -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Тип отчета</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>
</select>
</div>
<!-- Тип отчета -->
<div class="flex flex-col">
<label class="text-xs text-gray-500">Тип отчета</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>
</select>
<!-- Таблица -->
<div class="flex flex-col col-span-2 md:col-span-1">
<label class="text-xs text-gray-500">Таблица SQL</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>
<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>
<input type="number" v-model.number="daysBack" min="1" class="input-field py-1 text-sm">
</div>
<!-- Опции справа -->
<div class="flex items-center justify-end gap-2 h-full col-span-2 md:col-span-3 lg:col-span-2">
<label
class="flex items-center gap-2 px-2.5 h-[30px] rounded-md border cursor-pointer transition hover:bg-gray-50 text-xs"
:class="buildSummary ? 'bg-primary-50 border-primary-300 text-primary-700' : 'border-gray-200 text-gray-600'">
<input type="checkbox" v-model="buildSummary" class="hidden">
<div class="w-3.5 h-3.5 rounded border flex items-center justify-center transition"
:class="buildSummary ? 'bg-primary-600 border-primary-600' : 'border-gray-400 bg-white'">
<svg v-if="buildSummary" class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span class="font-medium">Summary</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"
:class="active ? 'bg-green-50 border-green-300 text-green-700' : 'border-gray-200 text-gray-600'">
<input type="checkbox" v-model="active" class="hidden">
<div class="w-3.5 h-3.5 rounded border flex items-center justify-center transition"
:class="active ? 'bg-green-600 border-green-600' : 'border-gray-400 bg-white'">
<svg v-if="active" class="w-2.5 h-2.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
</div>
<span class="font-medium">Активно</span>
</label>
</div>
</div>
<!-- Таблица -->
<div class="flex flex-col col-span-2 md:col-span-1">
<label class="text-xs text-gray-500">Таблица SQL</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>
<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>
<input type="number" v-model.number="daysBack" min="1" class="input-field py-1 text-sm">
</div>
<!-- Опции справа -->
<div class="flex items-center justify-end gap-2 h-full col-span-2 md:col-span-3 lg:col-span-2">
<!-- Summary -->
<label
class="flex items-center gap-2 px-2.5 h-[30px] rounded-md border cursor-pointer transition
hover:bg-gray-50 text-xs"
:class="buildSummary
? 'bg-primary-50 border-primary-300 text-primary-700'
: 'border-gray-200 text-gray-600'">
<input type="checkbox" v-model="buildSummary" class="hidden">
<div class="w-3.5 h-3.5 rounded border flex items-center justify-center transition"
:class="buildSummary
? 'bg-primary-600 border-primary-600'
: 'border-gray-400 bg-white'">
<svg v-if="buildSummary"
class="w-2.5 h-2.5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
<!-- Дополнительные настройки: имя запроса, рестораны, БД -->
<div class="mt-4 pt-3 border-t border-gray-100">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Имя запроса -->
<div>
<label class="text-xs text-gray-500">Имя запроса</label>
<input type="text" v-model="queryName" placeholder="Например: Продажи по дням" class="input-field py-1.5 text-sm w-full" />
</div>
<span class="font-medium">Summary</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"
:class="active
? 'bg-green-50 border-green-300 text-green-700'
: 'border-gray-200 text-gray-600'">
<input type="checkbox" v-model="active" class="hidden">
<div class="w-3.5 h-3.5 rounded border flex items-center justify-center transition"
:class="active
? 'bg-green-600 border-green-600'
: 'border-gray-400 bg-white'">
<svg v-if="active"
class="w-2.5 h-2.5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/>
</svg>
<!-- Рестораны -->
<div>
<label class="text-xs text-gray-500">Рестораны (можно несколько)</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>
<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>
</button>
<div v-if="selectedRestaurants.length" class="flex flex-wrap gap-1 mt-1">
<span v-for="r in selectedRestaurants" :key="r.id" class="text-xs bg-gray-100 px-1.5 py-0.5 rounded">{{ r.name }}</span>
</div>
</div>
<span class="font-medium">Активно</span>
</label>
<!-- Подключение к БД -->
<div>
<label class="text-xs text-gray-500">Подключение к БД</label>
<button @click="openDbConnectionModal" class="btn-secondary py-1.5 text-sm w-full flex justify-between items-center">
<span>{{ selectedDbConnection ? selectedDbConnection.name : 'Выбрать подключение' }}</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>
</button>
</div>
</div>
</div>
</div>
@@ -385,6 +391,113 @@
</div>
</div>
</Transition>
<!-- Модалка выбора ресторанов (множественный) -->
<Transition name="fade">
<div v-if="restaurantModal.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeRestaurantModal">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
<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>
<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" />
</svg>
</button>
</div>
<div class="p-4 border-b">
<div class="relative">
<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="Поиск по названию или хосту" />
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2">
<div v-for="rest in filteredRestaurantsList" :key="rest.id"
@click="toggleRestaurantSelection(rest)"
class="flex items-center justify-between p-4 rounded-xl border cursor-pointer transition-all hover:shadow-md"
:class="isRestaurantSelected(rest) ? 'border-primary-500 bg-primary-50' : 'border-gray-200'">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<p class="font-medium text-gray-900">{{ rest.name }}</p>
<p class="text-sm text-gray-500">{{ rest.host }}</p>
</div>
</div>
<div v-if="isRestaurantSelected(rest)" class="text-primary-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div v-if="filteredRestaurantsList.length === 0" class="text-center py-8 text-gray-500">
Рестораны не найдены
</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>
</div>
</div>
</div>
</div>
</Transition>
<!-- Модалка выбора подключения к БД (одиночный) -->
<Transition name="fade">
<div v-if="dbConnectionModal.show" class="fixed inset-0 z-50 overflow-y-auto" @click.self="closeDbConnectionModal">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm"></div>
<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>
<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" />
</svg>
</button>
</div>
<div class="p-4 border-b">
<div class="relative">
<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="Поиск по имени" />
</div>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2">
<div v-for="conn in filteredDbConnectionsList" :key="conn.id"
@click="selectDbConnection(conn)"
class="flex items-center justify-between p-4 rounded-xl border cursor-pointer transition-all hover:shadow-md"
:class="tempSelectedDbConnection?.id === conn.id ? 'border-primary-500 bg-primary-50' : 'border-gray-200'">
<div class="flex flex-col">
<p class="font-medium text-gray-900">{{ conn.name }}</p>
<p class="text-sm text-gray-500">{{ conn.type }} | {{ conn.host }}:{{ conn.port }}/{{ conn.database }}</p>
</div>
<div v-if="tempSelectedDbConnection?.id === conn.id" class="text-primary-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
<div v-if="filteredDbConnectionsList.length === 0" class="text-center py-8 text-gray-500">
Подключения не найдены
</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>
</div>
</div>
</div>
</div>
</Transition>
</AppLayout>
</template>
@@ -395,7 +508,7 @@ import { useNotification } from '@/composables/useNotification'
const { showNotification } = useNotification()
// Типы (без изменений)
// Типы
interface ApiColumn {
fieldKey: string
fieldKeyNormal: string
@@ -456,7 +569,23 @@ interface IikoConfig {
filters: Record<string, IikoConfigFilter>
}
// Refs
interface Restaurant {
id: number
name: string
host: string
}
interface DbConnection {
id: number
name: string
type: string
host: string
port: number
database: string
user: string
}
// Состояния
const columnsData = ref<ApiColumn[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
@@ -477,22 +606,126 @@ const reportType = ref<'SALES' | 'DELIVERIES' | 'TRANSACTIONS'>('SALES')
const buildSummary = ref(false)
const searchQuery = ref('')
const activeTab = ref<'table' | 'sql'>('table')
const collapsed = ref({ number: false, category: false, filter: false })
// Состояние сворачивания секций
const collapsed = ref({
number: false,
category: false,
filter: false
const queryName = ref('') // имя запроса
const selectedRestaurants = ref<Restaurant[]>([]) // выбранные рестораны
const selectedDbConnection = ref<DbConnection | null>(null) // выбранное подключение к БД
// Модалка ресторанов
const restaurantModal = ref({
show: false,
search: ''
})
const restaurantsList = ref<Restaurant[]>([])
const tempSelectedRestaurants = ref<Restaurant[]>([])
// Модалка подключения к БД
const dbConnectionModal = ref({
show: false,
search: ''
})
const dbConnectionsList = ref<DbConnection[]>([])
const tempSelectedDbConnection = ref<DbConnection | null>(null)
// Вспомогательные вычисляемые свойства для модалок
const filteredRestaurantsList = computed(() => {
if (!restaurantModal.value.search.trim()) return restaurantsList.value
const lower = restaurantModal.value.search.toLowerCase()
return restaurantsList.value.filter(r =>
r.name.toLowerCase().includes(lower) || r.host.toLowerCase().includes(lower)
)
})
const filteredDbConnectionsList = computed(() => {
if (!dbConnectionModal.value.search.trim()) return dbConnectionsList.value
const lower = dbConnectionModal.value.search.toLowerCase()
return dbConnectionsList.value.filter(c => c.name.toLowerCase().includes(lower))
})
// Функции для ресторанов
const openRestaurantModal = async () => {
await loadRestaurants()
// Копируем текущие выбранные рестораны во временный массив
tempSelectedRestaurants.value = [...selectedRestaurants.value]
restaurantModal.value.search = ''
restaurantModal.value.show = true
}
const closeRestaurantModal = () => {
restaurantModal.value.show = false
}
const isRestaurantSelected = (rest: Restaurant) => {
return tempSelectedRestaurants.value.some(r => r.id === rest.id)
}
const toggleRestaurantSelection = (rest: Restaurant) => {
const idx = tempSelectedRestaurants.value.findIndex(r => r.id === rest.id)
if (idx === -1) {
tempSelectedRestaurants.value.push(rest)
} else {
tempSelectedRestaurants.value.splice(idx, 1)
}
}
const confirmRestaurants = () => {
selectedRestaurants.value = [...tempSelectedRestaurants.value]
closeRestaurantModal()
showNotification('Рестораны выбраны', 'success')
}
// Функции для подключения к БД
const openDbConnectionModal = async () => {
await loadDbConnections()
tempSelectedDbConnection.value = selectedDbConnection.value ? { ...selectedDbConnection.value } : null
dbConnectionModal.value.search = ''
dbConnectionModal.value.show = true
}
const closeDbConnectionModal = () => {
dbConnectionModal.value.show = false
}
const selectDbConnection = (conn: DbConnection) => {
tempSelectedDbConnection.value = conn
}
const confirmDbConnection = () => {
selectedDbConnection.value = tempSelectedDbConnection.value ? { ...tempSelectedDbConnection.value } : null
closeDbConnectionModal()
showNotification('Подключение к БД выбрано', 'success')
}
// Загрузка данных для модалок
const loadRestaurants = async () => {
try {
const res = await fetch('/api/admin/restaurants')
if (!res.ok) throw new Error('Ошибка загрузки ресторанов')
const data = await res.json()
restaurantsList.value = data
} catch (err: any) {
showNotification('Не удалось загрузить список ресторанов', 'error')
}
}
const loadDbConnections = async () => {
try {
const res = await fetch('/api/admin/database-connections')
if (!res.ok) throw new Error('Ошибка загрузки подключений')
const data = await res.json()
dbConnectionsList.value = data
} catch (err: any) {
showNotification('Не удалось загрузить список подключений к БД', 'error')
}
}
const toggleSection = (section: 'number' | 'category' | 'filter') => {
collapsed.value[section] = !collapsed.value[section]
}
// Модалка сброса
const resetModal = ref({ show: false })
// Drag & drop state
let draggedItem: AvailableField | null = null
let draggedFromSidebar = true
const dragOverZone = ref<string | null>(null)
@@ -784,6 +1017,9 @@ const confirmReset = () => {
tableNameTouched.value = false
buildSummary.value = false
searchQuery.value = ''
queryName.value = ''
selectedRestaurants.value = []
selectedDbConnection.value = null
showNotification('Все настройки сброшены', 'success')
}