Redone Routes customers,contracts,contacts,vendors
This commit is contained in:
276
src/routes/resources/contacts.ts
Normal file
276
src/routes/resources/contacts.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import {
|
||||
eq,
|
||||
ilike,
|
||||
asc,
|
||||
desc,
|
||||
and,
|
||||
count,
|
||||
inArray,
|
||||
or
|
||||
} from "drizzle-orm"
|
||||
|
||||
import {
|
||||
contacts,
|
||||
customers,
|
||||
vendors
|
||||
} from "../../../db/schema"
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 🔍 Helper für SQL-Suche über mehrere Spalten
|
||||
// -------------------------------------------------------------
|
||||
function buildSearchCondition(table: any, columns: string[], search: string) {
|
||||
if (!search || !columns.length) return null
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
const conditions = columns
|
||||
.map(c => table[c])
|
||||
.filter(Boolean)
|
||||
.map(col => ilike(col, term))
|
||||
|
||||
if (conditions.length === 0) return null
|
||||
|
||||
// @ts-ignore
|
||||
return or(...conditions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default async function contactsRoutes(server: FastifyInstance) {
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// LIST
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contacts", async (req, reply) => {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId)
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const { search, sort, asc: ascQuery } = req.query as {
|
||||
search?: string
|
||||
sort?: string
|
||||
asc?: string
|
||||
}
|
||||
|
||||
// Grundfilter
|
||||
let whereCond: any = eq(contacts.tenant, tenantId)
|
||||
|
||||
// 🔍 Suche
|
||||
if (search) {
|
||||
const searchCond = buildSearchCondition(
|
||||
contacts,
|
||||
["firstName", "lastName", "email", "phone", "notes"],
|
||||
search
|
||||
)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
|
||||
// Query
|
||||
let q = server.db.select().from(contacts).where(whereCond)
|
||||
|
||||
// Sortierung
|
||||
if (sort) {
|
||||
const field = (contacts as any)[sort]
|
||||
if (field) {
|
||||
//@ts-ignore
|
||||
q = q.orderBy(
|
||||
ascQuery === "true" ? asc(field) : desc(field)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return await q
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PAGINATED
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contacts/paginated", async (req, reply) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId)
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const queryConfig = req.queryConfig
|
||||
const {
|
||||
pagination,
|
||||
sort,
|
||||
filters,
|
||||
paginationDisabled
|
||||
} = queryConfig
|
||||
|
||||
const { search, distinctColumns } = req.query as {
|
||||
search?: string
|
||||
distinctColumns?: string
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// WHERE CONDITIONS
|
||||
// -----------------------------------
|
||||
let whereCond: any = eq(contacts.tenant, tenantId)
|
||||
|
||||
// Filter
|
||||
if (filters) {
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
const col = (contacts as any)[key]
|
||||
if (!col) continue
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
whereCond = and(whereCond, inArray(col, val))
|
||||
} else {
|
||||
whereCond = and(whereCond, eq(col, val as any))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔍 Suche
|
||||
if (search && search.trim().length > 0) {
|
||||
const searchCond = buildSearchCondition(
|
||||
contacts,
|
||||
["firstName", "lastName", "email", "phone", "notes"],
|
||||
search
|
||||
)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// COUNT
|
||||
// -----------------------------------
|
||||
const totalRes = await server.db
|
||||
.select({ value: count(contacts.id) })
|
||||
.from(contacts)
|
||||
.where(whereCond)
|
||||
|
||||
const total = Number(totalRes[0]?.value ?? 0)
|
||||
|
||||
// -----------------------------------
|
||||
// DISTINCT VALUES
|
||||
// -----------------------------------
|
||||
const distinctValues: Record<string, any[]> = {}
|
||||
|
||||
if (distinctColumns) {
|
||||
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
|
||||
const col = (contacts as any)[colName]
|
||||
if (!col) continue
|
||||
|
||||
const rows = await server.db
|
||||
.select({ v: col })
|
||||
.from(contacts)
|
||||
.where(eq(contacts.tenant, tenantId))
|
||||
|
||||
distinctValues[colName] =
|
||||
[...new Set(rows.map(r => r.v).filter(v => v !== null && v !== ""))]
|
||||
.sort()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// PAGINATION
|
||||
// -----------------------------------
|
||||
let offset = pagination?.offset ?? 0
|
||||
let limit = pagination?.limit ?? 999999
|
||||
|
||||
// -----------------------------------
|
||||
// ORDER
|
||||
// -----------------------------------
|
||||
let orderField = null
|
||||
let orderDirection: "asc" | "desc" = "asc"
|
||||
|
||||
if (sort?.length > 0) {
|
||||
const s = sort[0]
|
||||
const col = (contacts as any)[s.field]
|
||||
if (col) {
|
||||
orderField = col
|
||||
orderDirection = s.direction === "asc" ? "asc" : "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// DATA QUERY
|
||||
// -----------------------------------
|
||||
let dataQuery = server.db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(whereCond)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
|
||||
if (orderField) {
|
||||
//@ts-ignore
|
||||
dataQuery =
|
||||
orderDirection === "asc"
|
||||
? dataQuery.orderBy(asc(orderField))
|
||||
: dataQuery.orderBy(desc(orderField))
|
||||
}
|
||||
|
||||
const data = await dataQuery
|
||||
|
||||
return {
|
||||
data,
|
||||
queryConfig: {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages: pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
: 1,
|
||||
distinctValues,
|
||||
search: search || null
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DETAIL (customer + vendor)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contacts/:id", async (req, reply) => {
|
||||
const { id } = req.params as { id: string }
|
||||
const tenantId = req.user?.tenant_id
|
||||
|
||||
if (!tenantId)
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(
|
||||
and(
|
||||
eq(contacts.id, Number(id)),
|
||||
eq(contacts.tenant, tenantId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!rows.length)
|
||||
return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const contact = rows[0]
|
||||
|
||||
const [customerRecord, vendorRecord] = await Promise.all([
|
||||
contact.customer
|
||||
? server.db.select().from(customers).where(eq(customers.id, contact.customer))
|
||||
: [],
|
||||
contact.vendor
|
||||
? server.db.select().from(vendors).where(eq(vendors.id, contact.vendor))
|
||||
: [],
|
||||
])
|
||||
|
||||
return {
|
||||
...contact,
|
||||
customer: customerRecord[0] ?? null,
|
||||
vendor: vendorRecord[0] ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
244
src/routes/resources/contracts.ts
Normal file
244
src/routes/resources/contracts.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import {
|
||||
eq,
|
||||
ilike,
|
||||
asc,
|
||||
desc,
|
||||
and,
|
||||
count,
|
||||
inArray,
|
||||
or
|
||||
} from "drizzle-orm"
|
||||
|
||||
import {
|
||||
contracts,
|
||||
customers,
|
||||
files
|
||||
} from "../../../db/schema"
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Helper: SQL‐LIKE Suche über mehrere Felder
|
||||
// -------------------------------------------------------------
|
||||
function buildSearchCondition(table: any, columns: string[], search?: string) {
|
||||
if (!search) return null
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
const conditions = columns
|
||||
.map(col => table[col])
|
||||
.filter(Boolean)
|
||||
.map(col => ilike(col, term))
|
||||
|
||||
if (conditions.length === 0) return null
|
||||
|
||||
// @ts-ignore
|
||||
return or(...conditions)
|
||||
}
|
||||
|
||||
export default async function contractsRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// LIST
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contracts", async (req, reply) => {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const { search, sort, asc: ascQuery } = req.query as {
|
||||
search?: string
|
||||
sort?: string
|
||||
asc?: string
|
||||
}
|
||||
|
||||
let whereCond: any = eq(contracts.tenant, tenantId)
|
||||
|
||||
// SQL SEARCH
|
||||
const searchCond = buildSearchCondition(
|
||||
contracts,
|
||||
["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||
search
|
||||
)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
|
||||
// Query
|
||||
let q = server.db
|
||||
.select()
|
||||
.from(contracts)
|
||||
.where(whereCond)
|
||||
|
||||
// SORT
|
||||
if (sort) {
|
||||
const field = (contracts as any)[sort]
|
||||
if (field) {
|
||||
//@ts-ignore
|
||||
q = q.orderBy(
|
||||
ascQuery === "true" ? asc(field) : desc(field)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return await q
|
||||
})
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PAGINATED
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contracts/paginated", async (req, reply) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const queryConfig = req.queryConfig
|
||||
const { pagination, sort, filters, paginationDisabled } = queryConfig
|
||||
|
||||
const { search, distinctColumns } = req.query as {
|
||||
search?: string
|
||||
distinctColumns?: string
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// WHERE
|
||||
// -----------------------------------
|
||||
let whereCond: any = eq(contracts.tenant, tenantId)
|
||||
|
||||
if (filters) {
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
const col = (contracts as any)[key]
|
||||
if (!col) continue
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
whereCond = and(whereCond, inArray(col, val))
|
||||
} else {
|
||||
whereCond = and(whereCond, eq(col, val as any))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SQL SEARCH
|
||||
const searchCond = buildSearchCondition(
|
||||
contracts,
|
||||
["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
|
||||
search
|
||||
)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
|
||||
// -----------------------------------
|
||||
// COUNT
|
||||
// -----------------------------------
|
||||
const totalRes = await server.db
|
||||
.select({ value: count(contracts.id) })
|
||||
.from(contracts)
|
||||
.where(whereCond)
|
||||
|
||||
const total = Number(totalRes[0]?.value ?? 0)
|
||||
|
||||
// -----------------------------------
|
||||
// DISTINCT
|
||||
// -----------------------------------
|
||||
const distinctValues: Record<string, any[]> = {}
|
||||
|
||||
if (distinctColumns) {
|
||||
for (const colName of distinctColumns.split(",")) {
|
||||
const col = (contracts as any)[colName.trim()]
|
||||
if (!col) continue
|
||||
|
||||
const rows = await server.db
|
||||
.select({ v: col })
|
||||
.from(contracts)
|
||||
.where(eq(contracts.tenant, tenantId))
|
||||
|
||||
distinctValues[colName] =
|
||||
[...new Set(rows.map(r => r.v).filter(v => v != null && v !== ""))]
|
||||
.sort()
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// PAGINATION
|
||||
// -----------------------------------
|
||||
let offset = pagination?.offset ?? 0
|
||||
let limit = pagination?.limit ?? 999999
|
||||
|
||||
// -----------------------------------
|
||||
// SORT
|
||||
// -----------------------------------
|
||||
let orderField = null
|
||||
let orderDir: "asc" | "desc" = "asc"
|
||||
|
||||
if (sort?.length > 0) {
|
||||
const s = sort[0]
|
||||
const col = (contracts as any)[s.field]
|
||||
if (col) {
|
||||
orderField = col
|
||||
orderDir = s.direction === "asc" ? "asc" : "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------
|
||||
// QUERY DATA
|
||||
// -----------------------------------
|
||||
let q = server.db
|
||||
.select()
|
||||
.from(contracts)
|
||||
.where(whereCond)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
|
||||
if (orderField) {
|
||||
//@ts-ignore
|
||||
q = orderDir === "asc" ? q.orderBy(asc(orderField)) : q.orderBy(desc(orderField))
|
||||
}
|
||||
|
||||
const data = await q
|
||||
|
||||
return {
|
||||
data,
|
||||
queryConfig: {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages: pagination?.limit ? Math.ceil(total / pagination.limit) : 1,
|
||||
distinctValues,
|
||||
search: search || null,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DETAIL (+ JOINS)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/contracts/:id", async (req, reply) => {
|
||||
const { id } = req.params as { id: string }
|
||||
const tenantId = req.user?.tenant_id
|
||||
|
||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(contracts)
|
||||
.where(and(eq(contracts.id, Number(id)), eq(contracts.tenant, tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (!rows.length) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const contract = rows[0]
|
||||
|
||||
const [customerRecord, fileList] = await Promise.all([
|
||||
contract.customer
|
||||
? server.db.select().from(customers).where(eq(customers.id, contract.customer))
|
||||
: [],
|
||||
server.db.select().from(files).where(eq(files.contract, Number(id))),
|
||||
])
|
||||
|
||||
return {
|
||||
...contract,
|
||||
customer: customerRecord[0] ?? null,
|
||||
files: fileList,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
ilike,
|
||||
asc,
|
||||
desc,
|
||||
and, count, inArray,
|
||||
and,
|
||||
count,
|
||||
inArray,
|
||||
or,
|
||||
} from "drizzle-orm"
|
||||
|
||||
import {
|
||||
@@ -17,7 +20,29 @@ import {
|
||||
statementallocations,
|
||||
files,
|
||||
events,
|
||||
} from "../../../db/schema" // dein zentraler index.ts Export
|
||||
} from "../../../db/schema"
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 🔍 Helper für SQL-Suche über mehrere Spalten
|
||||
// -------------------------------------------------------------
|
||||
function buildSearchCondition(table: any, columns: string[], search: string) {
|
||||
if (!search || !columns.length) return null
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
const conditions = columns
|
||||
.map((colName) => table[colName])
|
||||
.filter(Boolean)
|
||||
.map((col) => ilike(col, term))
|
||||
|
||||
if (conditions.length === 0) return null
|
||||
|
||||
// @ts-ignore
|
||||
return or(...conditions)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default async function customerRoutes(server: FastifyInstance) {
|
||||
|
||||
@@ -36,38 +61,38 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
}
|
||||
|
||||
// Basisquery
|
||||
let whereCond: any = eq(customers.tenant, tenantId)
|
||||
|
||||
// 🔍 SQL-Suche
|
||||
if (search) {
|
||||
const searchCond = buildSearchCondition(
|
||||
customers,
|
||||
["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
search
|
||||
)
|
||||
|
||||
if (searchCond) {
|
||||
whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
}
|
||||
|
||||
let baseQuery = server.db
|
||||
.select()
|
||||
.from(customers)
|
||||
.where(eq(customers.tenant, tenantId))
|
||||
|
||||
// Suche
|
||||
if (search) {
|
||||
baseQuery = server.db
|
||||
.select()
|
||||
.from(customers)
|
||||
.where(
|
||||
and(
|
||||
eq(customers.tenant, tenantId),
|
||||
ilike(customers.name, `%${search}%`)
|
||||
)
|
||||
)
|
||||
}
|
||||
.where(whereCond)
|
||||
|
||||
// Sortierung
|
||||
if (sort) {
|
||||
const field = (customers as any)[sort]
|
||||
if (field) {
|
||||
// @ts-ignore
|
||||
//@ts-ignore
|
||||
baseQuery = baseQuery.orderBy(
|
||||
ascQuery === "true" ? asc(field) : desc(field)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const customerList = await baseQuery
|
||||
|
||||
return customerList
|
||||
return await baseQuery
|
||||
})
|
||||
|
||||
|
||||
@@ -91,22 +116,19 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
} = queryConfig
|
||||
|
||||
const {
|
||||
select,
|
||||
search,
|
||||
searchColumns,
|
||||
distinctColumns
|
||||
} = req.query as {
|
||||
select?: string
|
||||
search?: string
|
||||
searchColumns?: string
|
||||
distinctColumns?: string
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// WHERE CONDITIONS
|
||||
// WHERE CONDITIONS (Basis)
|
||||
// ----------------------------
|
||||
let whereCond: any = eq(customers.tenant, tenantId)
|
||||
|
||||
// Filters
|
||||
if (filters) {
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
const col = (customers as any)[key]
|
||||
@@ -114,24 +136,29 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
whereCond = and(whereCond, inArray(col, val))
|
||||
} else if (val === true || val === false || val === null) {
|
||||
whereCond = and(whereCond, eq(col, val))
|
||||
} else {
|
||||
whereCond = and(whereCond, eq(col, val))
|
||||
whereCond = and(whereCond, eq(col, val as any))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// 🔍 SEARCH
|
||||
// ----------------------------
|
||||
if (search && search.trim().length > 0) {
|
||||
const searchTerm = `%${search.trim().toLowerCase()}%`
|
||||
whereCond = and(
|
||||
whereCond,
|
||||
ilike(customers.name, searchTerm)
|
||||
const searchCond = buildSearchCondition(
|
||||
customers,
|
||||
["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
search.trim()
|
||||
)
|
||||
|
||||
if (searchCond) {
|
||||
whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// COUNT FIX (Drizzle-safe)
|
||||
// COUNT
|
||||
// ----------------------------
|
||||
const totalRes = await server.db
|
||||
.select({ value: count(customers.id) })
|
||||
@@ -141,7 +168,7 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
const total = Number(totalRes[0]?.value ?? 0)
|
||||
|
||||
// ----------------------------
|
||||
// DISTINCT VALUES (optional)
|
||||
// DISTINCT VALUES
|
||||
// ----------------------------
|
||||
const distinctValues: Record<string, any[]> = {}
|
||||
|
||||
@@ -155,11 +182,9 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
.from(customers)
|
||||
.where(eq(customers.tenant, tenantId))
|
||||
|
||||
const values = rows
|
||||
.map(r => r.v)
|
||||
.filter(v => v != null && v !== "")
|
||||
|
||||
distinctValues[colName] = [...new Set(values)].sort()
|
||||
distinctValues[colName] =
|
||||
[...new Set(rows.map(r => r.v).filter(v => v != null && v !== ""))]
|
||||
.sort()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +225,7 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
.limit(limit)
|
||||
|
||||
if (orderField) {
|
||||
//@ts-ignore
|
||||
dataQuery =
|
||||
orderDirection === "asc"
|
||||
? dataQuery.orderBy(asc(orderField))
|
||||
@@ -209,7 +235,7 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
const data = await dataQuery
|
||||
|
||||
// ----------------------------
|
||||
// BUILD RETURN CONFIG
|
||||
// CONFIG RESPONSE
|
||||
// ----------------------------
|
||||
const totalPages = pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
@@ -231,7 +257,6 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -269,28 +294,11 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
customerFiles,
|
||||
customerEvents,
|
||||
] = await Promise.all([
|
||||
// Projekte, die dem Kunden zugeordnet sind
|
||||
server.db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.customer, Number(id))),
|
||||
server.db.select().from(projects).where(eq(projects.customer, Number(id))),
|
||||
server.db.select().from(plants).where(eq(plants.customer, Number(id))),
|
||||
server.db.select().from(contracts).where(eq(contracts.customer, Number(id))),
|
||||
server.db.select().from(contacts).where(eq(contacts.customer, Number(id))),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(plants)
|
||||
.where(eq(plants.customer, Number(id))),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(contracts)
|
||||
.where(eq(contracts.customer, Number(id))),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.customer, Number(id))),
|
||||
|
||||
// createddocuments + inner join statementallocations
|
||||
server.db
|
||||
.select({
|
||||
...createddocuments,
|
||||
@@ -303,15 +311,8 @@ export default async function customerRoutes(server: FastifyInstance) {
|
||||
)
|
||||
.where(eq(createddocuments.customer, Number(id))),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.customer, Number(id))),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq(events.customer, Number(id))),
|
||||
server.db.select().from(files).where(eq(files.customer, Number(id))),
|
||||
server.db.select().from(events).where(eq(events.customer, Number(id))),
|
||||
])
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
desc,
|
||||
and,
|
||||
count,
|
||||
inArray
|
||||
inArray,
|
||||
or
|
||||
} from "drizzle-orm"
|
||||
|
||||
import {
|
||||
@@ -15,6 +16,22 @@ import {
|
||||
files,
|
||||
} from "../../../db/schema"
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SQL Volltext-Suche (über mehrere relevante Felder)
|
||||
// -------------------------------------------------------------
|
||||
function buildVendorSearchTerm(search?: string) {
|
||||
if (!search) return null
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
return or(
|
||||
ilike(vendors.name, term),
|
||||
ilike(vendors.vendorNumber, term),
|
||||
ilike(vendors.notes, term),
|
||||
ilike(vendors.defaultPaymentMethod, term)
|
||||
)
|
||||
}
|
||||
|
||||
export default async function vendorRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -31,36 +48,30 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
asc?: string
|
||||
}
|
||||
|
||||
let baseQuery = server.db
|
||||
// WHERE
|
||||
let whereCond: any = eq(vendors.tenant, tenantId)
|
||||
|
||||
const searchCond = buildVendorSearchTerm(search)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
|
||||
// QUERY
|
||||
let q = server.db
|
||||
.select()
|
||||
.from(vendors)
|
||||
.where(eq(vendors.tenant, tenantId))
|
||||
.where(whereCond)
|
||||
|
||||
// 🔎 Suche
|
||||
if (search) {
|
||||
baseQuery = server.db
|
||||
.select()
|
||||
.from(vendors)
|
||||
.where(
|
||||
and(
|
||||
eq(vendors.tenant, tenantId),
|
||||
ilike(vendors.name, `%${search}%`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// 🔽 Sortierung
|
||||
// SORT
|
||||
if (sort) {
|
||||
const field = (vendors as any)[sort]
|
||||
if (field) {
|
||||
// @ts-ignore
|
||||
baseQuery = baseQuery.orderBy(
|
||||
ascQuery === "true" ? asc(field) : desc(field)
|
||||
)
|
||||
const col = (vendors as any)[sort]
|
||||
if (col) {
|
||||
//@ts-ignore
|
||||
q = ascQuery === "true"
|
||||
? q.orderBy(asc(col))
|
||||
: q.orderBy(desc(col))
|
||||
}
|
||||
}
|
||||
|
||||
return await baseQuery
|
||||
return await q
|
||||
} catch (err) {
|
||||
console.error("ERROR /resource/vendors:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
@@ -74,35 +85,22 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
server.get("/resource/vendors/paginated", async (req, reply) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const queryConfig = req.queryConfig
|
||||
const {
|
||||
pagination,
|
||||
sort,
|
||||
filters,
|
||||
paginationDisabled
|
||||
} = queryConfig
|
||||
const { pagination, sort, filters, paginationDisabled } = queryConfig
|
||||
|
||||
const {
|
||||
select,
|
||||
search,
|
||||
searchColumns,
|
||||
distinctColumns
|
||||
} = req.query as {
|
||||
select?: string
|
||||
const { search, distinctColumns } = req.query as {
|
||||
search?: string
|
||||
searchColumns?: string
|
||||
distinctColumns?: string
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// WHERE CONDITIONS
|
||||
// ----------------------------
|
||||
// ------------------------------------
|
||||
// WHERE
|
||||
// ------------------------------------
|
||||
let whereCond: any = eq(vendors.tenant, tenantId)
|
||||
|
||||
// Filters
|
||||
if (filters) {
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
const col = (vendors as any)[key]
|
||||
@@ -110,24 +108,19 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
whereCond = and(whereCond, inArray(col, val))
|
||||
} else if (val === true || val === false || val === null) {
|
||||
whereCond = and(whereCond, eq(col, val))
|
||||
} else {
|
||||
whereCond = and(whereCond, eq(col, val))
|
||||
whereCond = and(whereCond, eq(col, val as any))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (search && search.trim().length > 0) {
|
||||
whereCond = and(
|
||||
whereCond,
|
||||
ilike(vendors.name, `%${search.trim().toLowerCase()}%`)
|
||||
)
|
||||
}
|
||||
// SEARCH
|
||||
const searchCond = buildVendorSearchTerm(search)
|
||||
if (searchCond) whereCond = and(whereCond, searchCond)
|
||||
|
||||
// ----------------------------
|
||||
// ------------------------------------
|
||||
// COUNT
|
||||
// ----------------------------
|
||||
// ------------------------------------
|
||||
const totalRes = await server.db
|
||||
.select({ value: count(vendors.id) })
|
||||
.from(vendors)
|
||||
@@ -135,14 +128,14 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
|
||||
const total = Number(totalRes[0]?.value ?? 0)
|
||||
|
||||
// ----------------------------
|
||||
// DISTINCT FILTER VALUES
|
||||
// ----------------------------
|
||||
// ------------------------------------
|
||||
// DISTINCT VALUES
|
||||
// ------------------------------------
|
||||
const distinctValues: Record<string, any[]> = {}
|
||||
|
||||
if (distinctColumns) {
|
||||
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
|
||||
const col = (vendors as any)[colName]
|
||||
for (const field of distinctColumns.split(",")) {
|
||||
const col = (vendors as any)[field.trim()]
|
||||
if (!col) continue
|
||||
|
||||
const rows = await server.db
|
||||
@@ -152,42 +145,37 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
|
||||
const values = rows
|
||||
.map(r => r.v)
|
||||
.filter(v => v != null && v !== "")
|
||||
.filter(v => v !== null && v !== "")
|
||||
|
||||
distinctValues[colName] = [...new Set(values)].sort()
|
||||
distinctValues[field] = [...new Set(values)].sort()
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// ------------------------------------
|
||||
// PAGINATION
|
||||
// ----------------------------
|
||||
let offset = 0
|
||||
let limit = 999999
|
||||
// ------------------------------------
|
||||
const offset = pagination?.offset ?? 0
|
||||
const limit = pagination?.limit ?? 5000
|
||||
|
||||
if (!paginationDisabled && pagination) {
|
||||
offset = pagination.offset
|
||||
limit = pagination.limit
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// ORDER BY
|
||||
// ----------------------------
|
||||
let orderField = null
|
||||
let orderDirection: "asc" | "desc" = "asc"
|
||||
// ------------------------------------
|
||||
// SORT
|
||||
// ------------------------------------
|
||||
let orderField: any = null
|
||||
let orderDir: "asc" | "desc" = "asc"
|
||||
|
||||
if (sort?.length > 0) {
|
||||
const s = sort[0]
|
||||
const col = (vendors as any)[s.field]
|
||||
if (col) {
|
||||
orderField = col
|
||||
orderDirection = s.direction === "asc" ? "asc" : "desc"
|
||||
orderDir = s.direction === "asc" ? "asc" : "desc"
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// QUERY DATA
|
||||
// ----------------------------
|
||||
let dataQuery = server.db
|
||||
// ------------------------------------
|
||||
// DATA QUERY
|
||||
// ------------------------------------
|
||||
let q = server.db
|
||||
.select()
|
||||
.from(vendors)
|
||||
.where(whereCond)
|
||||
@@ -195,27 +183,27 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
.limit(limit)
|
||||
|
||||
if (orderField) {
|
||||
dataQuery =
|
||||
orderDirection === "asc"
|
||||
? dataQuery.orderBy(asc(orderField))
|
||||
: dataQuery.orderBy(desc(orderField))
|
||||
//@ts-ignore
|
||||
q = orderDir === "asc"
|
||||
? q.orderBy(asc(orderField))
|
||||
: q.orderBy(desc(orderField))
|
||||
}
|
||||
|
||||
const data = await dataQuery
|
||||
const data = await q
|
||||
|
||||
const totalPages = pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
: 1
|
||||
|
||||
const enrichedConfig = {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages,
|
||||
distinctValues,
|
||||
search: search || null,
|
||||
return {
|
||||
data,
|
||||
queryConfig: {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages: pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
: 1,
|
||||
distinctValues,
|
||||
search: search || null
|
||||
}
|
||||
}
|
||||
|
||||
return { data, queryConfig: enrichedConfig }
|
||||
} catch (err) {
|
||||
console.error("ERROR /resource/vendors/paginated:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
@@ -223,59 +211,38 @@ export default async function vendorRoutes(server: FastifyInstance) {
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DETAIL (mit JOINS)
|
||||
// DETAIL (mit JOINs: contacts, files)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/vendors/:id", async (req, reply) => {
|
||||
try {
|
||||
const { id } = req.params as { id: string }
|
||||
const tenantId = req.user?.tenant_id
|
||||
|
||||
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||
|
||||
const vendorId = Number(id)
|
||||
|
||||
const vendorRecord = await server.db
|
||||
const vendorRows = await server.db
|
||||
.select()
|
||||
.from(vendors)
|
||||
.where(
|
||||
and(
|
||||
eq(vendors.id, vendorId),
|
||||
eq(vendors.tenant, tenantId)
|
||||
)
|
||||
)
|
||||
.where(and(eq(vendors.id, vendorId), eq(vendors.tenant, tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (!vendorRecord.length) {
|
||||
return reply.code(404).send({ error: "Vendor not found" })
|
||||
}
|
||||
if (!vendorRows.length) return reply.code(404).send({ error: "Vendor not found" })
|
||||
|
||||
const vendor = vendorRecord[0]
|
||||
const vendor = vendorRows[0]
|
||||
|
||||
// ----------------------------
|
||||
// RELATIONS
|
||||
// ----------------------------
|
||||
const [
|
||||
vendorContacts,
|
||||
vendorFiles,
|
||||
] = await Promise.all([
|
||||
server.db
|
||||
.select()
|
||||
.from(contacts)
|
||||
.where(eq(contacts.vendor, vendorId)),
|
||||
|
||||
server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.vendor, vendorId)),
|
||||
const [vendorContacts, vendorFiles] = await Promise.all([
|
||||
server.db.select().from(contacts).where(eq(contacts.vendor, vendorId)),
|
||||
server.db.select().from(files).where(eq(files.vendor, vendorId))
|
||||
])
|
||||
|
||||
return {
|
||||
...vendor,
|
||||
contacts: vendorContacts,
|
||||
files: vendorFiles,
|
||||
files: vendorFiles
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR /resource/vendors/:id:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
|
||||
Reference in New Issue
Block a user