Converted Routes
This commit is contained in:
@@ -30,6 +30,11 @@ import notificationsRoutes from "./routes/notifications";
|
|||||||
import staffTimeRoutes from "./routes/staff/time";
|
import staffTimeRoutes from "./routes/staff/time";
|
||||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||||
|
|
||||||
|
//Resources
|
||||||
|
import customerRoutes from "./routes/resources/customers";
|
||||||
|
import vendorRoutes from "./routes/resources/vendors";
|
||||||
|
|
||||||
|
|
||||||
//M2M
|
//M2M
|
||||||
import authM2m from "./plugins/auth.m2m";
|
import authM2m from "./plugins/auth.m2m";
|
||||||
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
|
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
|
||||||
@@ -114,6 +119,10 @@ async function main() {
|
|||||||
await subApp.register(staffTimeRoutes);
|
await subApp.register(staffTimeRoutes);
|
||||||
await subApp.register(staffTimeConnectRoutes);
|
await subApp.register(staffTimeConnectRoutes);
|
||||||
|
|
||||||
|
|
||||||
|
await subApp.register(customerRoutes);
|
||||||
|
await subApp.register(vendorRoutes);
|
||||||
|
|
||||||
},{prefix: "/api"})
|
},{prefix: "/api"})
|
||||||
|
|
||||||
app.ready(async () => {
|
app.ready(async () => {
|
||||||
|
|||||||
@@ -1,79 +1,140 @@
|
|||||||
import { FastifyInstance } from "fastify";
|
import { FastifyInstance } from "fastify"
|
||||||
|
import {
|
||||||
|
authUsers,
|
||||||
|
authTenantUsers,
|
||||||
|
tenants,
|
||||||
|
authProfiles,
|
||||||
|
authUserRoles,
|
||||||
|
authRoles,
|
||||||
|
authRolePermissions,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
import { eq, and, or, isNull } from "drizzle-orm"
|
||||||
|
|
||||||
export default async function meRoutes(server: FastifyInstance) {
|
export default async function meRoutes(server: FastifyInstance) {
|
||||||
server.get("/me", async (req, reply) => {
|
server.get("/me", async (req, reply) => {
|
||||||
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
|
try {
|
||||||
|
const authUser = req.user
|
||||||
|
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
return reply.code(401).send({ error: "Unauthorized" })
|
return reply.code(401).send({ error: "Unauthorized" })
|
||||||
}
|
}
|
||||||
|
|
||||||
const user_id = req.user.user_id
|
const userId = authUser.user_id
|
||||||
const tenant_id = req.user.tenant_id
|
const activeTenantId = authUser.tenant_id
|
||||||
|
|
||||||
// 1. User laden
|
// ----------------------------------------------------
|
||||||
const { data: user, error: userError } = await server.supabase
|
// 1) USER LADEN
|
||||||
.from("auth_users")
|
// ----------------------------------------------------
|
||||||
.select("id, email, created_at, must_change_password")
|
const userResult = await server.db
|
||||||
.eq("id", authUser.user_id)
|
.select({
|
||||||
.single()
|
id: authUsers.id,
|
||||||
|
email: authUsers.email,
|
||||||
|
created_at: authUsers.created_at,
|
||||||
|
must_change_password: authUsers.must_change_password,
|
||||||
|
})
|
||||||
|
.from(authUsers)
|
||||||
|
.where(eq(authUsers.id, userId))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
if (userError || !user) {
|
const user = userResult[0]
|
||||||
return reply.code(401).send({ error: "User not found" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Tenants laden (alle Tenants des Users)
|
if (!user) {
|
||||||
const { data: tenantLinks, error: tenantLinksError } = await server.supabase
|
return reply.code(401).send({ error: "User not found" })
|
||||||
.from("auth_users")
|
}
|
||||||
.select(`*, tenants!auth_tenant_users ( id, name,short, locked, extraModules, businessInfo, numberRanges, dokuboxkey, standardEmailForInvoices, standardPaymentDays )`)
|
|
||||||
.eq("id", authUser.user_id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (tenantLinksError) {
|
// ----------------------------------------------------
|
||||||
|
// 2) TENANTS LADEN
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const tenantRows = await server.db
|
||||||
|
.select({
|
||||||
|
id: tenants.id,
|
||||||
|
name: tenants.name,
|
||||||
|
short: tenants.short,
|
||||||
|
locked: tenants.locked,
|
||||||
|
extraModules: tenants.extraModules,
|
||||||
|
businessInfo: tenants.businessInfo,
|
||||||
|
numberRanges: tenants.numberRanges,
|
||||||
|
dokuboxkey: tenants.dokuboxkey,
|
||||||
|
standardEmailForInvoices: tenants.standardEmailForInvoices,
|
||||||
|
standardPaymentDays: tenants.standardPaymentDays,
|
||||||
|
})
|
||||||
|
.from(authTenantUsers)
|
||||||
|
.innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id))
|
||||||
|
.where(eq(authTenantUsers.user_id, userId))
|
||||||
|
|
||||||
console.log(tenantLinksError)
|
const tenantList = tenantRows ?? []
|
||||||
|
|
||||||
return reply.code(401).send({ error: "Tenant Error" })
|
// ----------------------------------------------------
|
||||||
}
|
// 3) ACTIVE TENANT
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const activeTenant = activeTenantId
|
||||||
|
|
||||||
const tenants = tenantLinks?.tenants
|
// ----------------------------------------------------
|
||||||
|
// 4) PROFIL LADEN
|
||||||
|
// ----------------------------------------------------
|
||||||
|
let profile = null
|
||||||
|
if (activeTenantId) {
|
||||||
|
const profileResult = await server.db
|
||||||
|
.select()
|
||||||
|
.from(authProfiles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(authProfiles.user_id, userId),
|
||||||
|
eq(authProfiles.tenant_id, activeTenantId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
// 3. Aktiven Tenant bestimmen
|
profile = profileResult?.[0] ?? null
|
||||||
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
|
}
|
||||||
|
|
||||||
// 4. Profil für den aktiven Tenant laden
|
// ----------------------------------------------------
|
||||||
let profile = null
|
// 5) PERMISSIONS — RPC ERSETZT
|
||||||
if (activeTenant) {
|
// ----------------------------------------------------
|
||||||
const { data: profileData } = await server.supabase
|
const permissionRows =
|
||||||
.from("auth_profiles")
|
(await server.db
|
||||||
.select("*")
|
.select({
|
||||||
.eq("user_id", user.id)
|
permission: authRolePermissions.permission,
|
||||||
.eq("tenant_id", activeTenant)
|
})
|
||||||
.single()
|
.from(authUserRoles)
|
||||||
|
.innerJoin(
|
||||||
|
authRoles,
|
||||||
|
and(
|
||||||
|
eq(authRoles.id, authUserRoles.role_id),
|
||||||
|
or(
|
||||||
|
isNull(authRoles.tenant_id), // globale Rolle
|
||||||
|
eq(authRoles.tenant_id, activeTenantId) // tenant-spezifische Rolle
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
authRolePermissions,
|
||||||
|
eq(authRolePermissions.role_id, authRoles.id)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(authUserRoles.user_id, userId),
|
||||||
|
eq(authUserRoles.tenant_id, activeTenantId)
|
||||||
|
)
|
||||||
|
)) ?? []
|
||||||
|
|
||||||
profile = profileData
|
const permissions = Array.from(
|
||||||
}
|
new Set(permissionRows.map((p) => p.permission))
|
||||||
|
)
|
||||||
|
|
||||||
// 5. Permissions laden (über Funktion)
|
// ----------------------------------------------------
|
||||||
const { data: permissionsData, error: permissionsError } = await server.supabase
|
// RESPONSE
|
||||||
.rpc("auth_get_user_permissions", {
|
// ----------------------------------------------------
|
||||||
uid: user.id,
|
return {
|
||||||
tid: activeTenant || null
|
user,
|
||||||
})
|
tenants: tenantList,
|
||||||
|
activeTenant,
|
||||||
if(permissionsError) {
|
profile,
|
||||||
console.log(permissionsError)
|
permissions,
|
||||||
}
|
}
|
||||||
|
} catch (err: any) {
|
||||||
const permissions = permissionsData.map(i => i.permission) || []
|
console.error("ERROR in /me route:", err)
|
||||||
|
return reply.code(500).send({ error: "Internal server error" })
|
||||||
// 6. Response zurückgeben
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
tenants,
|
|
||||||
activeTenant,
|
|
||||||
profile,
|
|
||||||
permissions
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
155
src/routes/resources/_template.ts
Normal file
155
src/routes/resources/_template.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import { asc, desc, eq, ilike, and } from "drizzle-orm"
|
||||||
|
|
||||||
|
// Beispiel-Import — wird in der echten Datei ersetzt
|
||||||
|
import { exampleTable } from "../../../db/schema/exampleTable"
|
||||||
|
|
||||||
|
export default async function exampleRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// LIST (ohne Pagination)
|
||||||
|
// -------------------------------------------
|
||||||
|
server.get("/resource/example", 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 query = server.db
|
||||||
|
.select()
|
||||||
|
.from(exampleTable)
|
||||||
|
.where(eq(exampleTable.tenant, tenantId))
|
||||||
|
|
||||||
|
// 🔍 OPTIONAL: einfache Suche (LIKE)
|
||||||
|
if (search) {
|
||||||
|
query = server.db
|
||||||
|
.select()
|
||||||
|
.from(exampleTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(exampleTable.tenant, tenantId),
|
||||||
|
ilike(exampleTable.name as any, `%${search}%`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 Sortierung
|
||||||
|
if (sort) {
|
||||||
|
query = query.orderBy(
|
||||||
|
ascQuery === "true"
|
||||||
|
? asc((exampleTable as any)[sort])
|
||||||
|
: desc((exampleTable as any)[sort])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await query
|
||||||
|
return results
|
||||||
|
})
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// PAGINATED LIST
|
||||||
|
// -------------------------------------------
|
||||||
|
server.get("/resource/example/paginated", async (req, reply) => {
|
||||||
|
const tenantId = req.user?.tenant_id
|
||||||
|
if (!tenantId) return reply.code(400).send({ error: "No tenant selected" })
|
||||||
|
|
||||||
|
const {
|
||||||
|
offset = "0",
|
||||||
|
limit = "25",
|
||||||
|
search,
|
||||||
|
sort,
|
||||||
|
asc: ascQuery,
|
||||||
|
} = req.query as {
|
||||||
|
offset?: string
|
||||||
|
limit?: string
|
||||||
|
search?: string
|
||||||
|
sort?: string
|
||||||
|
asc?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetNum = parseInt(offset)
|
||||||
|
const limitNum = parseInt(limit)
|
||||||
|
|
||||||
|
// --- Basis WHERE
|
||||||
|
let whereClause: any = eq(exampleTable.tenant, tenantId)
|
||||||
|
|
||||||
|
// --- Suche
|
||||||
|
if (search) {
|
||||||
|
whereClause = and(
|
||||||
|
eq(exampleTable.tenant, tenantId),
|
||||||
|
ilike(exampleTable.name as any, `%${search}%`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 1. COUNT Query (total rows)
|
||||||
|
// -------------------------
|
||||||
|
const totalRowsResult = await server.db
|
||||||
|
.select({ count: server.db.fn.count(exampleTable.id) })
|
||||||
|
.from(exampleTable)
|
||||||
|
.where(whereClause)
|
||||||
|
|
||||||
|
const total = Number(totalRowsResult[0].count)
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// 2. DATA Query
|
||||||
|
// -------------------------
|
||||||
|
let dataQuery = server.db
|
||||||
|
.select()
|
||||||
|
.from(exampleTable)
|
||||||
|
.where(whereClause)
|
||||||
|
.offset(offsetNum)
|
||||||
|
.limit(limitNum)
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
dataQuery = dataQuery.orderBy(
|
||||||
|
ascQuery === "true"
|
||||||
|
? asc((exampleTable as any)[sort])
|
||||||
|
: desc((exampleTable as any)[sort])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await dataQuery
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: rows,
|
||||||
|
pagination: {
|
||||||
|
total,
|
||||||
|
offset: offsetNum,
|
||||||
|
limit: limitNum,
|
||||||
|
totalPages: Math.ceil(total / limitNum),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// -------------------------------------------
|
||||||
|
// DETAIL ROUTE
|
||||||
|
// -------------------------------------------
|
||||||
|
server.get("/resource/example/: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 result = await server.db
|
||||||
|
.select()
|
||||||
|
.from(exampleTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(exampleTable.id, id),
|
||||||
|
eq(exampleTable.tenant, tenantId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!result.length) {
|
||||||
|
return reply.code(404).send({ error: "Not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0]
|
||||||
|
})
|
||||||
|
}
|
||||||
328
src/routes/resources/customers.ts
Normal file
328
src/routes/resources/customers.ts
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import {
|
||||||
|
eq,
|
||||||
|
ilike,
|
||||||
|
asc,
|
||||||
|
desc,
|
||||||
|
and, count, inArray,
|
||||||
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
import {
|
||||||
|
customers,
|
||||||
|
projects,
|
||||||
|
plants,
|
||||||
|
contracts,
|
||||||
|
contacts,
|
||||||
|
createddocuments,
|
||||||
|
statementallocations,
|
||||||
|
files,
|
||||||
|
events,
|
||||||
|
} from "../../../db/schema" // dein zentraler index.ts Export
|
||||||
|
|
||||||
|
export default async function customerRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// LIST
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/resource/customers", 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basisquery
|
||||||
|
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}%`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortierung
|
||||||
|
if (sort) {
|
||||||
|
const field = (customers as any)[sort]
|
||||||
|
if (field) {
|
||||||
|
// @ts-ignore
|
||||||
|
baseQuery = baseQuery.orderBy(
|
||||||
|
ascQuery === "true" ? asc(field) : desc(field)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerList = await baseQuery
|
||||||
|
|
||||||
|
return customerList
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// PAGINATED
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/resource/customers/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 {
|
||||||
|
select,
|
||||||
|
search,
|
||||||
|
searchColumns,
|
||||||
|
distinctColumns
|
||||||
|
} = req.query as {
|
||||||
|
select?: string
|
||||||
|
search?: string
|
||||||
|
searchColumns?: string
|
||||||
|
distinctColumns?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// WHERE CONDITIONS
|
||||||
|
// ----------------------------
|
||||||
|
let whereCond: any = eq(customers.tenant, tenantId)
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
for (const [key, val] of Object.entries(filters)) {
|
||||||
|
const col = (customers as any)[key]
|
||||||
|
if (!col) continue
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search && search.trim().length > 0) {
|
||||||
|
const searchTerm = `%${search.trim().toLowerCase()}%`
|
||||||
|
whereCond = and(
|
||||||
|
whereCond,
|
||||||
|
ilike(customers.name, searchTerm)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// COUNT FIX (Drizzle-safe)
|
||||||
|
// ----------------------------
|
||||||
|
const totalRes = await server.db
|
||||||
|
.select({ value: count(customers.id) })
|
||||||
|
.from(customers)
|
||||||
|
.where(whereCond)
|
||||||
|
|
||||||
|
const total = Number(totalRes[0]?.value ?? 0)
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// DISTINCT VALUES (optional)
|
||||||
|
// ----------------------------
|
||||||
|
const distinctValues: Record<string, any[]> = {}
|
||||||
|
|
||||||
|
if (distinctColumns) {
|
||||||
|
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
|
||||||
|
const col = (customers as any)[colName]
|
||||||
|
if (!col) continue
|
||||||
|
|
||||||
|
const rows = await server.db
|
||||||
|
.select({ v: col })
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// PAGINATION
|
||||||
|
// ----------------------------
|
||||||
|
let offset = 0
|
||||||
|
let limit = 999999
|
||||||
|
|
||||||
|
if (!paginationDisabled && pagination) {
|
||||||
|
offset = pagination.offset
|
||||||
|
limit = pagination.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// ORDER BY
|
||||||
|
// ----------------------------
|
||||||
|
let orderField = null
|
||||||
|
let orderDirection: "asc" | "desc" = "asc"
|
||||||
|
|
||||||
|
if (sort?.length > 0) {
|
||||||
|
const s = sort[0]
|
||||||
|
const col = (customers as any)[s.field]
|
||||||
|
if (col) {
|
||||||
|
orderField = col
|
||||||
|
orderDirection = s.direction === "asc" ? "asc" : "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// QUERY DATA
|
||||||
|
// ----------------------------
|
||||||
|
let dataQuery = server.db
|
||||||
|
.select()
|
||||||
|
.from(customers)
|
||||||
|
.where(whereCond)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
|
||||||
|
if (orderField) {
|
||||||
|
dataQuery =
|
||||||
|
orderDirection === "asc"
|
||||||
|
? dataQuery.orderBy(asc(orderField))
|
||||||
|
: dataQuery.orderBy(desc(orderField))
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await dataQuery
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// BUILD RETURN CONFIG
|
||||||
|
// ----------------------------
|
||||||
|
const totalPages = pagination?.limit
|
||||||
|
? Math.ceil(total / pagination.limit)
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const enrichedConfig = {
|
||||||
|
...queryConfig,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
distinctValues,
|
||||||
|
search: search || null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
queryConfig: enrichedConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// DETAIL (mit ALLEN JOINS)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/resource/customers/: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" })
|
||||||
|
|
||||||
|
// --- 1) Customer selbst laden
|
||||||
|
const customerRecord = await server.db
|
||||||
|
.select()
|
||||||
|
.from(customers)
|
||||||
|
.where(and(eq(customers.id, Number(id)), eq(customers.tenant, tenantId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!customerRecord.length) {
|
||||||
|
return reply.code(404).send({ error: "Customer not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const customer = customerRecord[0]
|
||||||
|
|
||||||
|
|
||||||
|
// --- 2) Relations:
|
||||||
|
const [
|
||||||
|
customerProjects,
|
||||||
|
customerPlants,
|
||||||
|
customerContracts,
|
||||||
|
customerContacts,
|
||||||
|
customerDocuments,
|
||||||
|
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(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,
|
||||||
|
allocations: statementallocations,
|
||||||
|
})
|
||||||
|
.from(createddocuments)
|
||||||
|
.leftJoin(
|
||||||
|
statementallocations,
|
||||||
|
eq(statementallocations.cd_id, createddocuments.id)
|
||||||
|
)
|
||||||
|
.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))),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...customer,
|
||||||
|
projects: customerProjects,
|
||||||
|
plants: customerPlants,
|
||||||
|
contracts: customerContracts,
|
||||||
|
contacts: customerContacts,
|
||||||
|
createddocuments: customerDocuments,
|
||||||
|
files: customerFiles,
|
||||||
|
events: customerEvents,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
284
src/routes/resources/vendors.ts
Normal file
284
src/routes/resources/vendors.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import {
|
||||||
|
eq,
|
||||||
|
ilike,
|
||||||
|
asc,
|
||||||
|
desc,
|
||||||
|
and,
|
||||||
|
count,
|
||||||
|
inArray
|
||||||
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
import {
|
||||||
|
vendors,
|
||||||
|
contacts,
|
||||||
|
files,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
export default async function vendorRoutes(server: FastifyInstance) {
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// LIST
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
server.get("/resource/vendors", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
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 baseQuery = server.db
|
||||||
|
.select()
|
||||||
|
.from(vendors)
|
||||||
|
.where(eq(vendors.tenant, tenantId))
|
||||||
|
|
||||||
|
// 🔎 Suche
|
||||||
|
if (search) {
|
||||||
|
baseQuery = server.db
|
||||||
|
.select()
|
||||||
|
.from(vendors)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(vendors.tenant, tenantId),
|
||||||
|
ilike(vendors.name, `%${search}%`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔽 Sortierung
|
||||||
|
if (sort) {
|
||||||
|
const field = (vendors as any)[sort]
|
||||||
|
if (field) {
|
||||||
|
// @ts-ignore
|
||||||
|
baseQuery = baseQuery.orderBy(
|
||||||
|
ascQuery === "true" ? asc(field) : desc(field)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await baseQuery
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR /resource/vendors:", err)
|
||||||
|
return reply.code(500).send({ error: "Internal Server Error" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// PAGINATED
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
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" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryConfig = req.queryConfig
|
||||||
|
const {
|
||||||
|
pagination,
|
||||||
|
sort,
|
||||||
|
filters,
|
||||||
|
paginationDisabled
|
||||||
|
} = queryConfig
|
||||||
|
|
||||||
|
const {
|
||||||
|
select,
|
||||||
|
search,
|
||||||
|
searchColumns,
|
||||||
|
distinctColumns
|
||||||
|
} = req.query as {
|
||||||
|
select?: string
|
||||||
|
search?: string
|
||||||
|
searchColumns?: string
|
||||||
|
distinctColumns?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// WHERE CONDITIONS
|
||||||
|
// ----------------------------
|
||||||
|
let whereCond: any = eq(vendors.tenant, tenantId)
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
for (const [key, val] of Object.entries(filters)) {
|
||||||
|
const col = (vendors as any)[key]
|
||||||
|
if (!col) continue
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search && search.trim().length > 0) {
|
||||||
|
whereCond = and(
|
||||||
|
whereCond,
|
||||||
|
ilike(vendors.name, `%${search.trim().toLowerCase()}%`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// COUNT
|
||||||
|
// ----------------------------
|
||||||
|
const totalRes = await server.db
|
||||||
|
.select({ value: count(vendors.id) })
|
||||||
|
.from(vendors)
|
||||||
|
.where(whereCond)
|
||||||
|
|
||||||
|
const total = Number(totalRes[0]?.value ?? 0)
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// DISTINCT FILTER VALUES
|
||||||
|
// ----------------------------
|
||||||
|
const distinctValues: Record<string, any[]> = {}
|
||||||
|
|
||||||
|
if (distinctColumns) {
|
||||||
|
for (const colName of distinctColumns.split(",").map(v => v.trim())) {
|
||||||
|
const col = (vendors as any)[colName]
|
||||||
|
if (!col) continue
|
||||||
|
|
||||||
|
const rows = await server.db
|
||||||
|
.select({ v: col })
|
||||||
|
.from(vendors)
|
||||||
|
.where(eq(vendors.tenant, tenantId))
|
||||||
|
|
||||||
|
const values = rows
|
||||||
|
.map(r => r.v)
|
||||||
|
.filter(v => v != null && v !== "")
|
||||||
|
|
||||||
|
distinctValues[colName] = [...new Set(values)].sort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// PAGINATION
|
||||||
|
// ----------------------------
|
||||||
|
let offset = 0
|
||||||
|
let limit = 999999
|
||||||
|
|
||||||
|
if (!paginationDisabled && pagination) {
|
||||||
|
offset = pagination.offset
|
||||||
|
limit = pagination.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// ORDER BY
|
||||||
|
// ----------------------------
|
||||||
|
let orderField = null
|
||||||
|
let orderDirection: "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// QUERY DATA
|
||||||
|
// ----------------------------
|
||||||
|
let dataQuery = server.db
|
||||||
|
.select()
|
||||||
|
.from(vendors)
|
||||||
|
.where(whereCond)
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
|
||||||
|
if (orderField) {
|
||||||
|
dataQuery =
|
||||||
|
orderDirection === "asc"
|
||||||
|
? dataQuery.orderBy(asc(orderField))
|
||||||
|
: dataQuery.orderBy(desc(orderField))
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await dataQuery
|
||||||
|
|
||||||
|
const totalPages = pagination?.limit
|
||||||
|
? Math.ceil(total / pagination.limit)
|
||||||
|
: 1
|
||||||
|
|
||||||
|
const enrichedConfig = {
|
||||||
|
...queryConfig,
|
||||||
|
total,
|
||||||
|
totalPages,
|
||||||
|
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" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// DETAIL (mit JOINS)
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
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
|
||||||
|
.select()
|
||||||
|
.from(vendors)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(vendors.id, vendorId),
|
||||||
|
eq(vendors.tenant, tenantId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!vendorRecord.length) {
|
||||||
|
return reply.code(404).send({ error: "Vendor not found" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const vendor = vendorRecord[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)),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...vendor,
|
||||||
|
contacts: vendorContacts,
|
||||||
|
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