Search und Save Function
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 33s
Build and Push Docker Images / build-frontend (push) Successful in 1m8s

This commit is contained in:
2026-02-18 15:04:16 +01:00
parent 6fded3993a
commit 844af30b18
4 changed files with 228 additions and 27 deletions

View File

@@ -7,7 +7,8 @@ import {
and,
count,
inArray,
or
or,
sql,
} from "drizzle-orm"
import { resourceConfig } from "../../utils/resource.config";
@@ -23,15 +24,66 @@ import { decrypt, encrypt } from "../../utils/crypt";
function buildSearchCondition(columns: any[], search: string) {
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)
.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 {
@@ -78,6 +130,13 @@ function applyResourceWhereFilters(resource: string, table: any, whereCond: any)
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>) {
const infoData = payload.infoData && typeof payload.infoData === "object" ? payload.infoData : {}
const normalized = {
@@ -295,6 +354,8 @@ export default async function resourceRoutes(server: FastifyInstance) {
let whereCond: any = eq(table.tenant, tenantId);
whereCond = applyResourceWhereFilters(resource, table, whereCond)
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 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));
if (relConfig.searchColumns) {
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 (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());
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)) {
const col = (table as any)[key];
if (!col) continue;
parsedFilters.push({ key, value: val })
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();
}
}
@@ -595,8 +694,15 @@ export default async function resourceRoutes(server: FastifyInstance) {
}
Object.keys(data).forEach((key) => {
if ((key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) && key !== "deliveryDateType") {
data[key] = normalizeDate(data[key])
const value = 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)
}
})

View File

@@ -48,13 +48,13 @@ export const resourceConfig = {
numberRangeHolder: "projectNumber"
},
customers: {
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"],
table: customers,
numberRangeHolder: "customerNumber",
},
members: {
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
table: customers,
numberRangeHolder: "customerNumber",

View File

@@ -77,6 +77,7 @@ const sort = ref({
const columnsToFilter = ref({})
const showMobileFilter = ref(false)
let lastSearchRequestId = 0
//Functions
@@ -86,6 +87,7 @@ function resetMobileFilters() {
Object.keys(itemsMeta.value.distinctValues).forEach(key => {
columnsToFilter.value[key] = [...itemsMeta.value.distinctValues[key]]
tempStore.clearFilter(type, key)
})
showMobileFilter.value = false
@@ -94,7 +96,16 @@ function resetMobileFilters() {
function applyMobileFilters() {
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
@@ -108,7 +119,7 @@ const clearSearchString = () => {
}
const performSearch = async () => {
tempStore.modifySearchString(type,searchString)
tempStore.modifySearchString(type, searchString.value)
changePage(1,true)
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
const setupPage = async () => {
const currentRequestId = ++lastSearchRequestId
loading.value = true
setPageLayout(platformIsNative ? "mobile" : "default")
@@ -159,9 +226,9 @@ const setupPage = async () => {
archived:false
}
Object.keys(columnsToFilter.value).forEach((column) => {
if(columnsToFilter.value[column].length !== itemsMeta.value.distinctValues[column].length) {
filters[column] = columnsToFilter.value[column]
Object.entries(tempStore.filters[type] || {}).forEach(([column, selected]) => {
if (Array.isArray(selected) && selected.length > 0) {
filters[column] = selected
}
})
@@ -177,16 +244,16 @@ const setupPage = async () => {
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
itemsMeta.value = meta
if(!initialSetupDone.value){
Object.keys(tempStore.filters[type] || {}).forEach((column) => {
columnsToFilter.value[column] = tempStore.filters[type][column]
})
Object.keys(itemsMeta.value.distinctValues).filter(i => !Object.keys(tempStore.filters[type] || {}).includes(i)).forEach(distinctValue => {
columnsToFilter.value[distinctValue] = itemsMeta.value.distinctValues[distinctValue]
})
initializeDistinctFilters()
} else {
syncDistinctFiltersFromStore()
}
@@ -199,9 +266,18 @@ setupPage()
const handleFilterChange = async (action,column) => {
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') {
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()
}

View File

@@ -49,6 +49,23 @@ export const useTempStore = defineStore('temp', () => {
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) {
columns.value[type] = input
storeTempConfig()
@@ -81,6 +98,8 @@ export const useTempStore = defineStore('temp', () => {
clearSearchString,
filters,
modifyFilter,
setFilters,
clearFilter,
columns,
modifyColumns,
modifyPages,
@@ -89,4 +108,4 @@ export const useTempStore = defineStore('temp', () => {
modifyBankingPeriod, // Neue Funktion exportiert
settings
}
})
})