Search und Save Function
This commit is contained in:
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user