#14-intelligent-groups #21
7 changed files with 119 additions and 16 deletions
|
@ -64,10 +64,11 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts" generic="T extends { id: number }">
|
<script setup lang="ts" generic="T extends { id: FieldType }">
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
import { ChevronRightIcon, ChevronLeftIcon, XMarkIcon } from "@heroicons/vue/20/solid";
|
||||||
import Spinner from "./Spinner.vue";
|
import Spinner from "./Spinner.vue";
|
||||||
|
import type { FieldType } from "@/types/dynamicQueries";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: { type: Array<T>, default: [] },
|
items: { type: Array<T>, default: [] },
|
||||||
|
@ -105,8 +106,8 @@ const emit = defineEmits({
|
||||||
search(search: string) {
|
search(search: string) {
|
||||||
return typeof search == "number";
|
return typeof search == "number";
|
||||||
},
|
},
|
||||||
clickRow(id: number) {
|
clickRow(id: FieldType) {
|
||||||
return typeof id == "number";
|
return true;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col border border-gray-300 rounded-md select-none">
|
<div class="flex flex-col border border-gray-300 rounded-md select-none">
|
||||||
<div class="flex flex-row gap-2 border-b border-gray-300 p-2">
|
<div class="flex flex-row max-lg:flex-wrap gap-2 border-b border-gray-300 p-2">
|
||||||
<div
|
<div
|
||||||
class="p-1 border border-gray-400 bg-green-200 rounded-md"
|
class="p-1 border border-gray-400 bg-green-200 rounded-md"
|
||||||
title="Abfrage starten"
|
title="Abfrage starten"
|
||||||
|
@ -26,7 +26,10 @@
|
||||||
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
<div v-if="allowPredefinedSelect && can('read', 'settings', 'query_store')" class="flex flex-row gap-2">
|
<div
|
||||||
|
v-if="allowPredefinedSelect && can('read', 'settings', 'query_store')"
|
||||||
|
class="flex flex-row gap-2 max-lg:w-full max-lg:order-10"
|
||||||
|
>
|
||||||
<select v-model="activeQueryId" class="max-h-[34px] !py-0">
|
<select v-model="activeQueryId" class="max-h-[34px] !py-0">
|
||||||
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option>
|
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option>
|
||||||
<option v-for="query in queries" :key="query.id" :value="query.id" @click="value = query.query">
|
<option v-for="query in queries" :key="query.id" :value="query.id" @click="value = query.query">
|
||||||
|
@ -42,8 +45,8 @@
|
||||||
<InboxArrowDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
<InboxArrowDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow"></div>
|
<div class="grow max-lg:hidden"></div>
|
||||||
<div class="flex flex-row overflow-hidden border border-gray-400 rounded-md">
|
<div class="flex flex-row min-w-fit overflow-hidden border border-gray-400 rounded-md">
|
||||||
<div
|
<div
|
||||||
class="p-1"
|
class="p-1"
|
||||||
:class="queryMode == 'structure' ? 'bg-gray-200' : ''"
|
:class="queryMode == 'structure' ? 'bg-gray-200' : ''"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<select v-model="foreignColumn" class="w-full">
|
<select v-model="foreignColumn" class="w-full">
|
||||||
<option value="" disabled>Relation auswählen</option>
|
<option value="" disabled>Relation auswählen</option>
|
||||||
<option v-for="relation in activeTable?.relations" :value="relation.column">
|
<option v-for="relation in activeTable?.relations" :value="relation.column">
|
||||||
{{ relation.column }} -> {{ relation.referencedTableName }}
|
{{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<Table v-model="value" disable-table-select />
|
<Table v-model="value" disable-table-select />
|
||||||
|
@ -24,6 +24,7 @@ import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||||
import Table from "./Table.vue";
|
import Table from "./Table.vue";
|
||||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { joinTableName } from "@/helpers/queryFormatter";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -79,7 +80,7 @@ export default defineComponent({
|
||||||
this.$emit("update:model-value", {
|
this.$emit("update:model-value", {
|
||||||
...this.modelValue,
|
...this.modelValue,
|
||||||
foreignColumn: val,
|
foreignColumn: val,
|
||||||
table: relTable?.referencedTableName, // TODO: use dictionary
|
table: joinTableName(relTable?.referencedTableName ?? ""),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
58
src/helpers/queryFormatter.ts
Normal file
58
src/helpers/queryFormatter.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { joinTableFormatter, type FieldType, type QueryResult } from "../types/dynamicQueries";
|
||||||
|
|
||||||
|
export function joinTableName(name: string): string {
|
||||||
|
let normalized = joinTableFormatter[name];
|
||||||
|
return normalized ?? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: string]: FieldType }> {
|
||||||
|
function flatten(row: QueryResult, prefix: string = ""): Array<{ [key: string]: FieldType }> {
|
||||||
|
let results: Array<{ [key: string]: FieldType }> = [{}];
|
||||||
|
|
||||||
|
for (const key in row) {
|
||||||
|
const value = row[key];
|
||||||
|
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
|
||||||
|
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
||||||
|
console.log(value, newKey);
|
||||||
|
const arrayResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
|
value.forEach((item) => {
|
||||||
|
const flattenedItems = flatten(item, newKey);
|
||||||
|
arrayResults.push(...flattenedItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
|
results.forEach((res) => {
|
||||||
|
arrayResults.forEach((arrRes) => {
|
||||||
|
tempResults.push({ ...res, ...arrRes });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
results = tempResults;
|
||||||
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
console.log(value, newKey);
|
||||||
|
const objResults = flatten(value as QueryResult, newKey);
|
||||||
|
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
|
results.forEach((res) => {
|
||||||
|
objResults.forEach((objRes) => {
|
||||||
|
tempResults.push({ ...res, ...objRes });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
results = tempResults;
|
||||||
|
} else {
|
||||||
|
results.forEach((res) => {
|
||||||
|
res[newKey] = String(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenedResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
|
|
||||||
|
result.forEach((item) => {
|
||||||
|
const flattenedItems = flatten(item);
|
||||||
|
flattenedResults.push(...flattenedItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
return flattenedResults;
|
||||||
|
}
|
|
@ -1,14 +1,15 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { TableMeta } from "../../viewmodels/admin/query.models";
|
import type { TableMeta } from "../../viewmodels/admin/query.models";
|
||||||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
import type { DynamicQueryStructure, FieldType } from "../../types/dynamicQueries";
|
||||||
|
import { flattenQueryResult } from "../../helpers/queryFormatter";
|
||||||
|
|
||||||
export const useQueryBuilderStore = defineStore("queryBuilder", {
|
export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
tableMetas: [] as Array<TableMeta>,
|
tableMetas: [] as Array<TableMeta>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
data: [] as Array<{ id: number; [key: string]: any }>,
|
data: [] as Array<{ id: FieldType; [key: string]: FieldType }>,
|
||||||
totalLength: 0 as number,
|
totalLength: 0 as number,
|
||||||
loadingData: "fetched" as "loading" | "fetched" | "failed",
|
loadingData: "fetched" as "loading" | "fetched" | "failed",
|
||||||
queryError: "" as string | { sql: string; code: string; msg: string },
|
queryError: "" as string | { sql: string; code: string; msg: string },
|
||||||
|
@ -43,7 +44,10 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.data.stats == "success") {
|
if (result.data.stats == "success") {
|
||||||
this.data = result.data.rows;
|
this.data = flattenQueryResult(result.data.rows).map((row) => ({
|
||||||
|
id: row.id ?? "", // Ensure id is present
|
||||||
|
...row,
|
||||||
|
}));
|
||||||
this.totalLength = result.data.total;
|
this.totalLength = result.data.total;
|
||||||
this.loadingData = "fetched";
|
this.loadingData = "fetched";
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,5 +66,28 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
this.queryError = "";
|
this.queryError = "";
|
||||||
this.loadingData = "fetched";
|
this.loadingData = "fetched";
|
||||||
},
|
},
|
||||||
|
exportData() {
|
||||||
|
if (this.data.length == 0) return;
|
||||||
|
const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))]
|
||||||
|
.map((e) => e.join(";"))
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// Create a Blob from the CSV string
|
||||||
|
const blob = new Blob([csvString], { type: "text/csv" });
|
||||||
|
|
||||||
|
// Create a download link
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "items.csv";
|
||||||
|
|
||||||
|
// Append the link to the document and trigger the download
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
document.body.removeChild(a);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -54,6 +54,10 @@ export type OrderByStructure = {
|
||||||
|
|
||||||
export type OrderByType = "ASC" | "DESC";
|
export type OrderByType = "ASC" | "DESC";
|
||||||
|
|
||||||
|
export type QueryResult = {
|
||||||
|
[key: string]: FieldType | QueryResult | Array<QueryResult>;
|
||||||
|
};
|
||||||
|
|
||||||
export const whereOperationArray = [
|
export const whereOperationArray = [
|
||||||
"eq",
|
"eq",
|
||||||
"neq",
|
"neq",
|
||||||
|
@ -72,3 +76,12 @@ export const whereOperationArray = [
|
||||||
"endsWith",
|
"endsWith",
|
||||||
"timespanEq",
|
"timespanEq",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const joinTableFormatter: { [key: string]: string } = {
|
||||||
|
member_awards: "memberAwards",
|
||||||
|
membership_status: "membershipStatus",
|
||||||
|
member_qualifications: "memberQualifications",
|
||||||
|
member_executive_positions: "memberExecutivePositions",
|
||||||
|
communication_type: "communicationType",
|
||||||
|
executive_position: "executivePosition",
|
||||||
|
};
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
@query:run="sendQuery"
|
@query:run="sendQuery"
|
||||||
@query:save="triggerSave"
|
@query:save="triggerSave"
|
||||||
@results:clear="clearResults"
|
@results:clear="clearResults"
|
||||||
@results:export=""
|
@results:export="exportData"
|
||||||
/>
|
/>
|
||||||
<p>Ergebnisse:</p>
|
<p>Ergebnisse:</p>
|
||||||
<div
|
<div
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
:indicateLoading="loadingData == 'loading'"
|
:indicateLoading="loadingData == 'loading'"
|
||||||
@load-data="(offset, count) => sendQuery(offset, count)"
|
@load-data="(offset, count) => sendQuery(offset, count)"
|
||||||
>
|
>
|
||||||
<template #pageRow="{ row }: { row: { id: number; [key: string]: any } }">
|
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
|
||||||
<p>{{ row }}</p>
|
<p>{{ row }}</p>
|
||||||
</template>
|
</template>
|
||||||
</Pagination>
|
</Pagination>
|
||||||
|
@ -63,7 +63,7 @@ import MainTemplate from "@/templates/Main.vue";
|
||||||
import Pagination from "@/components/Pagination.vue";
|
import Pagination from "@/components/Pagination.vue";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||||
import BuilderHost from "../../../../components/queryBuilder/BuilderHost.vue";
|
import BuilderHost from "../../../../components/queryBuilder/BuilderHost.vue";
|
||||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
import type { DynamicQueryStructure, FieldType } from "@/types/dynamicQueries";
|
||||||
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ export default defineComponent({
|
||||||
this.fetchTableMetas();
|
this.fetchTableMetas();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults"]),
|
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults", "exportData"]),
|
||||||
...mapActions(useQueryStoreStore, ["triggerSave"]),
|
...mapActions(useQueryStoreStore, ["triggerSave"]),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue