up OLAPConstructor.vue
This commit is contained in:
@@ -106,8 +106,8 @@
|
||||
|
||||
<!-- Правая часть -->
|
||||
<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>
|
||||
@@ -147,63 +147,69 @@
|
||||
|
||||
<!-- Опции справа -->
|
||||
<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'">
|
||||
|
||||
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">
|
||||
: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'">
|
||||
|
||||
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">
|
||||
: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="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>
|
||||
<!-- Рестораны -->
|
||||
<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>
|
||||
<!-- Подключение к БД -->
|
||||
<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>
|
||||
|
||||
<!-- Вкладки -->
|
||||
<div class="card overflow-hidden p-2">
|
||||
<div class="flex border-b border-gray-200">
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user