Search und Save Function
This commit is contained in:
@@ -7,7 +7,8 @@ import {
|
|||||||
and,
|
and,
|
||||||
count,
|
count,
|
||||||
inArray,
|
inArray,
|
||||||
or
|
or,
|
||||||
|
sql,
|
||||||
} from "drizzle-orm"
|
} from "drizzle-orm"
|
||||||
|
|
||||||
import { resourceConfig } from "../../utils/resource.config";
|
import { resourceConfig } from "../../utils/resource.config";
|
||||||
@@ -23,15 +24,66 @@ import { decrypt, encrypt } from "../../utils/crypt";
|
|||||||
function buildSearchCondition(columns: any[], search: string) {
|
function buildSearchCondition(columns: any[], search: string) {
|
||||||
if (!search || !columns.length) return null
|
if (!search || !columns.length) return null
|
||||||
|
|
||||||
const term = `%${search.toLowerCase()}%`
|
const normalizeForSearch = (value: string) =>
|
||||||
|
value
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/ß/g, "ss")
|
||||||
|
|
||||||
const conditions = columns
|
const searchTermsRaw = search
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.split(/\s+/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((col) => ilike(col, term))
|
|
||||||
|
|
||||||
if (conditions.length === 0) return null
|
const searchTermsNormalized = searchTermsRaw.map(normalizeForSearch)
|
||||||
|
|
||||||
return or(...conditions)
|
const normalizeSqlExpr = (valueExpr: any) => sql`
|
||||||
|
lower(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
replace(cast(${valueExpr} as text), 'Ä', 'A'),
|
||||||
|
'Ö', 'O'
|
||||||
|
),
|
||||||
|
'Ü', 'U'
|
||||||
|
),
|
||||||
|
'ä', 'a'
|
||||||
|
),
|
||||||
|
'ö', 'o'
|
||||||
|
),
|
||||||
|
'ü', 'u'
|
||||||
|
),
|
||||||
|
'ß', 'ss'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
const validColumns = columns.filter(Boolean)
|
||||||
|
if (validColumns.length === 0) return null
|
||||||
|
|
||||||
|
// Alle Suchspalten zu einem String zusammenführen, damit Vor-/Nachname zuverlässig
|
||||||
|
// gemeinsam durchsuchbar sind (auch wenn in getrennten Feldern gespeichert).
|
||||||
|
const combinedRawExpr = sql`concat_ws(' ', ${sql.join(validColumns.map((col) => sql`coalesce(cast(${col} as text), '')`), sql`, `)})`
|
||||||
|
const combinedNormalizedExpr = normalizeSqlExpr(combinedRawExpr)
|
||||||
|
|
||||||
|
const perTermConditions = searchTermsRaw.map((rawTerm, idx) => {
|
||||||
|
const normalizedTerm = searchTermsNormalized[idx]
|
||||||
|
const rawLike = `%${rawTerm}%`
|
||||||
|
const normalizedLike = `%${normalizedTerm}%`
|
||||||
|
|
||||||
|
const rawCondition = ilike(combinedRawExpr, rawLike)
|
||||||
|
const normalizedCondition = sql`${combinedNormalizedExpr} like ${normalizedLike}`
|
||||||
|
|
||||||
|
return or(rawCondition, normalizedCondition)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (perTermConditions.length === 0) return null
|
||||||
|
return and(...perTermConditions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDiffValue(value: any): string {
|
function formatDiffValue(value: any): string {
|
||||||
@@ -78,6 +130,13 @@ function applyResourceWhereFilters(resource: string, table: any, whereCond: any)
|
|||||||
return whereCond
|
return whereCond
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDateLikeField(key: string) {
|
||||||
|
if (key === "deliveryDateType") return false
|
||||||
|
if (key.includes("_at") || key.endsWith("At")) return true
|
||||||
|
if (/Date$/.test(key)) return true
|
||||||
|
return /(^|_|-)date($|_|-)/i.test(key)
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMemberPayload(payload: Record<string, any>) {
|
function normalizeMemberPayload(payload: Record<string, any>) {
|
||||||
const infoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
|
const infoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
|
||||||
const normalized = {
|
const normalized = {
|
||||||
@@ -295,6 +354,8 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
let whereCond: any = eq(table.tenant, tenantId);
|
let whereCond: any = eq(table.tenant, tenantId);
|
||||||
whereCond = applyResourceWhereFilters(resource, table, whereCond)
|
whereCond = applyResourceWhereFilters(resource, table, whereCond)
|
||||||
const searchCols: any[] = (config.searchColumns || []).map(c => table[c]);
|
const searchCols: any[] = (config.searchColumns || []).map(c => table[c]);
|
||||||
|
const debugSearchColumnNames: string[] = [...(config.searchColumns || [])];
|
||||||
|
const parsedFilters: Array<{ key: string; value: any }> = []
|
||||||
|
|
||||||
let countQuery = server.db.select({ value: count(table.id) }).from(table).$dynamic();
|
let countQuery = server.db.select({ value: count(table.id) }).from(table).$dynamic();
|
||||||
let mainQuery = server.db.select().from(table).$dynamic();
|
let mainQuery = server.db.select().from(table).$dynamic();
|
||||||
@@ -312,7 +373,10 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
mainQuery = mainQuery.leftJoin(relTable, eq(table[rel], relTable.id));
|
mainQuery = mainQuery.leftJoin(relTable, eq(table[rel], relTable.id));
|
||||||
if (relConfig.searchColumns) {
|
if (relConfig.searchColumns) {
|
||||||
relConfig.searchColumns.forEach(c => {
|
relConfig.searchColumns.forEach(c => {
|
||||||
if (relTable[c]) searchCols.push(relTable[c]);
|
if (relTable[c]) {
|
||||||
|
searchCols.push(relTable[c]);
|
||||||
|
debugSearchColumnNames.push(`${rel}.${c}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,6 +385,23 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
|
if (resource === "customers") {
|
||||||
|
const rawSearch = search.trim()
|
||||||
|
const terms = rawSearch.toLowerCase().split(/\s+/).filter(Boolean)
|
||||||
|
const normalizedTerms = terms
|
||||||
|
.map((t) => t.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/ß/g, "ss"))
|
||||||
|
|
||||||
|
server.log.info({
|
||||||
|
tag: "customer-search-debug",
|
||||||
|
search: rawSearch,
|
||||||
|
terms,
|
||||||
|
normalizedTerms,
|
||||||
|
searchColumns: debugSearchColumnNames,
|
||||||
|
page: pagination?.page ?? 1,
|
||||||
|
limit: pagination?.limit ?? 100,
|
||||||
|
}, "Paginated customer search request")
|
||||||
|
}
|
||||||
|
|
||||||
const searchCond = buildSearchCondition(searchCols, search.trim());
|
const searchCond = buildSearchCondition(searchCols, search.trim());
|
||||||
if (searchCond) whereCond = and(whereCond, searchCond);
|
if (searchCond) whereCond = and(whereCond, searchCond);
|
||||||
}
|
}
|
||||||
@@ -329,6 +410,7 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
for (const [key, val] of Object.entries(filters)) {
|
for (const [key, val] of Object.entries(filters)) {
|
||||||
const col = (table as any)[key];
|
const col = (table as any)[key];
|
||||||
if (!col) continue;
|
if (!col) continue;
|
||||||
|
parsedFilters.push({ key, value: val })
|
||||||
whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : and(whereCond, eq(col, val as any));
|
whereCond = Array.isArray(val) ? and(whereCond, inArray(col, val)) : and(whereCond, eq(col, val as any));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,7 +451,24 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const dRows = await distinctQuery.where(whereCond);
|
let distinctWhereCond: any = eq(table.tenant, tenantId)
|
||||||
|
distinctWhereCond = applyResourceWhereFilters(resource, table, distinctWhereCond)
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
const searchCond = buildSearchCondition(searchCols, search.trim())
|
||||||
|
if (searchCond) distinctWhereCond = and(distinctWhereCond, searchCond)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of parsedFilters) {
|
||||||
|
if (f.key === colName) continue
|
||||||
|
const filterCol = (table as any)[f.key]
|
||||||
|
if (!filterCol) continue
|
||||||
|
distinctWhereCond = Array.isArray(f.value)
|
||||||
|
? and(distinctWhereCond, inArray(filterCol, f.value))
|
||||||
|
: and(distinctWhereCond, eq(filterCol, f.value as any))
|
||||||
|
}
|
||||||
|
|
||||||
|
const dRows = await distinctQuery.where(distinctWhereCond);
|
||||||
distinctValues[colName] = [...new Set(dRows.map(r => r.v).filter(v => v != null && v !== ""))].sort();
|
distinctValues[colName] = [...new Set(dRows.map(r => r.v).filter(v => v != null && v !== ""))].sort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -595,8 +694,15 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") {
|
const value = data[key]
|
||||||
data[key] = normalizeDate(data[key])
|
const shouldNormalize =
|
||||||
|
isDateLikeField(key) &&
|
||||||
|
value !== null &&
|
||||||
|
value !== undefined &&
|
||||||
|
(typeof value === "string" || typeof value === "number" || value instanceof Date)
|
||||||
|
|
||||||
|
if (shouldNormalize) {
|
||||||
|
data[key] = normalizeDate(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ export const resourceConfig = {
|
|||||||
numberRangeHolder: "projectNumber"
|
numberRangeHolder: "projectNumber"
|
||||||
},
|
},
|
||||||
customers: {
|
customers: {
|
||||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
|
||||||
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"],
|
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"],
|
||||||
table: customers,
|
table: customers,
|
||||||
numberRangeHolder: "customerNumber",
|
numberRangeHolder: "customerNumber",
|
||||||
},
|
},
|
||||||
members: {
|
members: {
|
||||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
|
||||||
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
|
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
|
||||||
table: customers,
|
table: customers,
|
||||||
numberRangeHolder: "customerNumber",
|
numberRangeHolder: "customerNumber",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const sort = ref({
|
|||||||
const columnsToFilter = ref({})
|
const columnsToFilter = ref({})
|
||||||
|
|
||||||
const showMobileFilter = ref(false)
|
const showMobileFilter = ref(false)
|
||||||
|
let lastSearchRequestId = 0
|
||||||
|
|
||||||
|
|
||||||
//Functions
|
//Functions
|
||||||
@@ -86,6 +87,7 @@ function resetMobileFilters() {
|
|||||||
|
|
||||||
Object.keys(itemsMeta.value.distinctValues).forEach(key => {
|
Object.keys(itemsMeta.value.distinctValues).forEach(key => {
|
||||||
columnsToFilter.value[key] = [...itemsMeta.value.distinctValues[key]]
|
columnsToFilter.value[key] = [...itemsMeta.value.distinctValues[key]]
|
||||||
|
tempStore.clearFilter(type, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
showMobileFilter.value = false
|
showMobileFilter.value = false
|
||||||
@@ -94,7 +96,16 @@ function resetMobileFilters() {
|
|||||||
|
|
||||||
function applyMobileFilters() {
|
function applyMobileFilters() {
|
||||||
Object.keys(columnsToFilter.value).forEach(key => {
|
Object.keys(columnsToFilter.value).forEach(key => {
|
||||||
tempStore.modifyFilter(type, key, columnsToFilter.value[key])
|
const selected = columnsToFilter.value[key]
|
||||||
|
const available = itemsMeta.value?.distinctValues?.[key] || []
|
||||||
|
|
||||||
|
if (!Array.isArray(selected) || selected.length === 0 || selected.length === available.length) {
|
||||||
|
tempStore.clearFilter(type, key)
|
||||||
|
columnsToFilter.value[key] = [...available]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tempStore.modifyFilter(type, key, selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
showMobileFilter.value = false
|
showMobileFilter.value = false
|
||||||
@@ -108,7 +119,7 @@ const clearSearchString = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const performSearch = async () => {
|
const performSearch = async () => {
|
||||||
tempStore.modifySearchString(type,searchString)
|
tempStore.modifySearchString(type, searchString.value)
|
||||||
changePage(1,true)
|
changePage(1,true)
|
||||||
setupPage()
|
setupPage()
|
||||||
}
|
}
|
||||||
@@ -143,9 +154,65 @@ const isFiltered = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const initializeDistinctFilters = () => {
|
||||||
|
const distinctValues = itemsMeta.value?.distinctValues || {}
|
||||||
|
const storedDomainFilters = tempStore.filters[type] || {}
|
||||||
|
const normalizedStoredFilters = {}
|
||||||
|
|
||||||
|
// Nur gültige, noch vorhandene Filterwerte aus dem TempStore übernehmen.
|
||||||
|
Object.entries(distinctValues).forEach(([column, availableValues]) => {
|
||||||
|
const available = Array.isArray(availableValues) ? availableValues : []
|
||||||
|
const storedSelected = storedDomainFilters[column]
|
||||||
|
|
||||||
|
if (!Array.isArray(storedSelected)) {
|
||||||
|
columnsToFilter.value[column] = [...available]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = storedSelected.filter((value) => available.includes(value))
|
||||||
|
const isFullSelection = selected.length === 0 || selected.length === available.length
|
||||||
|
|
||||||
|
if (isFullSelection) {
|
||||||
|
columnsToFilter.value[column] = [...available]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
columnsToFilter.value[column] = selected
|
||||||
|
normalizedStoredFilters[column] = selected
|
||||||
|
})
|
||||||
|
|
||||||
|
// Persistiert bereinigte Filter nur einmalig bei der Initialisierung.
|
||||||
|
tempStore.setFilters(type, normalizedStoredFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncDistinctFiltersFromStore = () => {
|
||||||
|
const distinctValues = itemsMeta.value?.distinctValues || {}
|
||||||
|
const storedDomainFilters = tempStore.filters[type] || {}
|
||||||
|
|
||||||
|
Object.entries(distinctValues).forEach(([column, availableValues]) => {
|
||||||
|
const available = Array.isArray(availableValues) ? availableValues : []
|
||||||
|
const storedSelected = storedDomainFilters[column]
|
||||||
|
|
||||||
|
if (!Array.isArray(storedSelected) || storedSelected.length === 0) {
|
||||||
|
columnsToFilter.value[column] = [...available]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = storedSelected.filter((value) => available.includes(value))
|
||||||
|
if (selected.length === 0 || selected.length === available.length) {
|
||||||
|
columnsToFilter.value[column] = [...available]
|
||||||
|
tempStore.clearFilter(type, column)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
columnsToFilter.value[column] = selected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//SETUP
|
//SETUP
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
|
const currentRequestId = ++lastSearchRequestId
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setPageLayout(platformIsNative ? "mobile" : "default")
|
setPageLayout(platformIsNative ? "mobile" : "default")
|
||||||
|
|
||||||
@@ -159,9 +226,9 @@ const setupPage = async () => {
|
|||||||
archived:false
|
archived:false
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(columnsToFilter.value).forEach((column) => {
|
Object.entries(tempStore.filters[type] || {}).forEach(([column, selected]) => {
|
||||||
if(columnsToFilter.value[column].length !== itemsMeta.value.distinctValues[column].length) {
|
if (Array.isArray(selected) && selected.length > 0) {
|
||||||
filters[column] = columnsToFilter.value[column]
|
filters[column] = selected
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -177,16 +244,16 @@ const setupPage = async () => {
|
|||||||
distinctColumns: dataType.templateColumns.filter(i => i.distinct).map(i => i.key),
|
distinctColumns: dataType.templateColumns.filter(i => i.distinct).map(i => i.key),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Verhindert Race Conditions beim schnellen Tippen:
|
||||||
|
// Nur das Ergebnis des letzten Requests darf den State setzen.
|
||||||
|
if (currentRequestId !== lastSearchRequestId) return
|
||||||
|
|
||||||
items.value = data
|
items.value = data
|
||||||
itemsMeta.value = meta
|
itemsMeta.value = meta
|
||||||
if(!initialSetupDone.value){
|
if(!initialSetupDone.value){
|
||||||
Object.keys(tempStore.filters[type] || {}).forEach((column) => {
|
initializeDistinctFilters()
|
||||||
columnsToFilter.value[column] = tempStore.filters[type][column]
|
} else {
|
||||||
})
|
syncDistinctFiltersFromStore()
|
||||||
|
|
||||||
Object.keys(itemsMeta.value.distinctValues).filter(i => !Object.keys(tempStore.filters[type] || {}).includes(i)).forEach(distinctValue => {
|
|
||||||
columnsToFilter.value[distinctValue] = itemsMeta.value.distinctValues[distinctValue]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -199,9 +266,18 @@ setupPage()
|
|||||||
|
|
||||||
const handleFilterChange = async (action,column) => {
|
const handleFilterChange = async (action,column) => {
|
||||||
if(action === 'reset') {
|
if(action === 'reset') {
|
||||||
columnsToFilter.value[column] = itemsMeta.value.distinctValues[column]
|
columnsToFilter.value[column] = [...(itemsMeta.value.distinctValues?.[column] || [])]
|
||||||
|
tempStore.clearFilter(type, column)
|
||||||
} else if(action === 'change') {
|
} else if(action === 'change') {
|
||||||
tempStore.modifyFilter(type,column,columnsToFilter.value[column])
|
const selected = columnsToFilter.value[column]
|
||||||
|
const available = itemsMeta.value.distinctValues?.[column] || []
|
||||||
|
|
||||||
|
if (!Array.isArray(selected) || selected.length === 0 || selected.length === available.length) {
|
||||||
|
tempStore.clearFilter(type, column)
|
||||||
|
columnsToFilter.value[column] = [...available]
|
||||||
|
} else {
|
||||||
|
tempStore.modifyFilter(type,column,selected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setupPage()
|
setupPage()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,23 @@ export const useTempStore = defineStore('temp', () => {
|
|||||||
storeTempConfig()
|
storeTempConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFilters(domain, nextFilters) {
|
||||||
|
filters.value[domain] = nextFilters
|
||||||
|
if (!filters.value[domain] || Object.keys(filters.value[domain]).length === 0) {
|
||||||
|
delete filters.value[domain]
|
||||||
|
}
|
||||||
|
storeTempConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilter(domain, type) {
|
||||||
|
if (!filters.value[domain]) return
|
||||||
|
delete filters.value[domain][type]
|
||||||
|
if (Object.keys(filters.value[domain]).length === 0) {
|
||||||
|
delete filters.value[domain]
|
||||||
|
}
|
||||||
|
storeTempConfig()
|
||||||
|
}
|
||||||
|
|
||||||
function modifyColumns(type, input) {
|
function modifyColumns(type, input) {
|
||||||
columns.value[type] = input
|
columns.value[type] = input
|
||||||
storeTempConfig()
|
storeTempConfig()
|
||||||
@@ -81,6 +98,8 @@ export const useTempStore = defineStore('temp', () => {
|
|||||||
clearSearchString,
|
clearSearchString,
|
||||||
filters,
|
filters,
|
||||||
modifyFilter,
|
modifyFilter,
|
||||||
|
setFilters,
|
||||||
|
clearFilter,
|
||||||
columns,
|
columns,
|
||||||
modifyColumns,
|
modifyColumns,
|
||||||
modifyPages,
|
modifyPages,
|
||||||
@@ -89,4 +108,4 @@ export const useTempStore = defineStore('temp', () => {
|
|||||||
modifyBankingPeriod, // Neue Funktion exportiert
|
modifyBankingPeriod, // Neue Funktion exportiert
|
||||||
settings
|
settings
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user