10
db/index.ts
Normal file
10
db/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { drizzle } from "drizzle-orm/node-postgres"
|
||||
import { Pool } from "pg"
|
||||
import {secrets} from "../src/utils/secrets";
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: secrets.DATABASE_URL,
|
||||
max: 10, // je nach Last
|
||||
})
|
||||
|
||||
export const db = drizzle(pool)
|
||||
24
db/schema/accounts.ts
Normal file
24
db/schema/accounts.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
export const accounts = pgTable("accounts", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
number: text("number").notNull(),
|
||||
label: text("label").notNull(),
|
||||
|
||||
description: text("description"),
|
||||
})
|
||||
|
||||
export type Account = typeof accounts.$inferSelect
|
||||
export type NewAccount = typeof accounts.$inferInsert
|
||||
83
db/schema/auth_profiles.ts
Normal file
83
db/schema/auth_profiles.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
timestamp,
|
||||
date,
|
||||
boolean,
|
||||
bigint,
|
||||
doublePrecision,
|
||||
jsonb,
|
||||
} from "drizzle-orm/pg-core"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const authProfiles = pgTable("auth_profiles", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
user_id: uuid("user_id").references(() => authUsers.id),
|
||||
|
||||
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
first_name: text("first_name").notNull(),
|
||||
last_name: text("last_name").notNull(),
|
||||
|
||||
full_name: text("full_name").generatedAlwaysAs(
|
||||
`((first_name || ' ') || last_name)`
|
||||
),
|
||||
|
||||
mobile_tel: text("mobile_tel"),
|
||||
fixed_tel: text("fixed_tel"),
|
||||
salutation: text("salutation"),
|
||||
employee_number: text("employee_number"),
|
||||
|
||||
weekly_working_hours: doublePrecision("weekly_working_hours").default(0),
|
||||
annual_paid_leave_days: bigint("annual_paid_leave_days", { mode: "number" }),
|
||||
|
||||
weekly_regular_working_hours: jsonb("weekly_regular_working_hours").default("{}"),
|
||||
|
||||
clothing_size_top: text("clothing_size_top"),
|
||||
clothing_size_bottom: text("clothing_size_bottom"),
|
||||
clothing_size_shoe: text("clothing_size_shoe"),
|
||||
|
||||
email_signature: text("email_signature").default("<p>Mit freundlichen Grüßen</p>"),
|
||||
|
||||
birthday: date("birthday"),
|
||||
entry_date: date("entry_date").defaultNow(),
|
||||
|
||||
automatic_hour_corrections: jsonb("automatic_hour_corrections").default("[]"),
|
||||
|
||||
recreation_days_compensation: boolean("recreation_days_compensation")
|
||||
.notNull()
|
||||
.default(true),
|
||||
|
||||
customer_for_portal: bigint("customer_for_portal", { mode: "number" }),
|
||||
|
||||
pinned_on_navigation: jsonb("pinned_on_navigation").notNull().default("[]"),
|
||||
|
||||
email: text("email"),
|
||||
token_id: text("token_id"),
|
||||
|
||||
weekly_working_days: doublePrecision("weekly_working_days"),
|
||||
|
||||
old_profile_id: uuid("old_profile_id"),
|
||||
temp_config: jsonb("temp_config"),
|
||||
|
||||
state_code: text("state_code").default("DE-NI"),
|
||||
|
||||
contract_type: text("contract_type"),
|
||||
position: text("position"),
|
||||
qualification: text("qualification"),
|
||||
|
||||
address_street: text("address_street"),
|
||||
address_zip: text("address_zip"),
|
||||
address_city: text("address_city"),
|
||||
|
||||
active: boolean("active").notNull().default(true),
|
||||
})
|
||||
|
||||
export type AuthProfile = typeof authProfiles.$inferSelect
|
||||
export type NewAuthProfile = typeof authProfiles.$inferInsert
|
||||
23
db/schema/auth_role_permisssions.ts
Normal file
23
db/schema/auth_role_permisssions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"
|
||||
import { authRoles } from "./auth_roles"
|
||||
|
||||
export const authRolePermissions = pgTable(
|
||||
"auth_role_permissions",
|
||||
{
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
role_id: uuid("role_id")
|
||||
.notNull()
|
||||
.references(() => authRoles.id),
|
||||
|
||||
permission: text("permission").notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
primaryKey: [table.role_id, table.permission],
|
||||
})
|
||||
)
|
||||
|
||||
export type AuthRolePermission = typeof authRolePermissions.$inferSelect
|
||||
export type NewAuthRolePermission = typeof authRolePermissions.$inferInsert
|
||||
19
db/schema/auth_roles.ts
Normal file
19
db/schema/auth_roles.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { pgTable, uuid, text, timestamp, bigint } from "drizzle-orm/pg-core"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const authRoles = pgTable("auth_roles", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
|
||||
created_by: uuid("created_by").references(() => authUsers.id),
|
||||
tenant_id: bigint("tenant_id", {mode: "number"}),
|
||||
})
|
||||
|
||||
export type AuthRole = typeof authRoles.$inferSelect
|
||||
export type NewAuthRole = typeof authRoles.$inferInsert
|
||||
22
db/schema/auth_tenant_users.ts
Normal file
22
db/schema/auth_tenant_users.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const authTenantUsers = pgTable(
|
||||
"auth_tenant_users",
|
||||
{
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||
user_id: uuid("user_id").notNull(),
|
||||
|
||||
created_by: uuid("created_by").references(() => authUsers.id),
|
||||
},
|
||||
(table) => ({
|
||||
primaryKey: [table.tenant_id, table.user_id],
|
||||
})
|
||||
)
|
||||
|
||||
export type AuthTenantUser = typeof authTenantUsers.$inferSelect
|
||||
export type NewAuthTenantUser = typeof authTenantUsers.$inferInsert
|
||||
30
db/schema/auth_user_roles.ts
Normal file
30
db/schema/auth_user_roles.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { authRoles } from "./auth_roles"
|
||||
|
||||
export const authUserRoles = pgTable(
|
||||
"auth_user_roles",
|
||||
{
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
user_id: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id),
|
||||
|
||||
role_id: uuid("role_id")
|
||||
.notNull()
|
||||
.references(() => authRoles.id),
|
||||
|
||||
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||
|
||||
created_by: uuid("created_by").references(() => authUsers.id),
|
||||
},
|
||||
(table) => ({
|
||||
primaryKey: [table.user_id, table.role_id, table.tenant_id],
|
||||
})
|
||||
)
|
||||
|
||||
export type AuthUserRole = typeof authUserRoles.$inferSelect
|
||||
export type NewAuthUserRole = typeof authUserRoles.$inferInsert
|
||||
22
db/schema/auth_users.ts
Normal file
22
db/schema/auth_users.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { pgTable, uuid, text, boolean, timestamp } from "drizzle-orm/pg-core"
|
||||
|
||||
export const authUsers = pgTable("auth_users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
email: text("email").notNull(),
|
||||
passwordHash: text("password_hash").notNull(),
|
||||
|
||||
multiTenant: boolean("multi_tenant").notNull().default(true),
|
||||
must_change_password: boolean("must_change_password").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
ported: boolean("ported").notNull().default(true),
|
||||
})
|
||||
|
||||
export type AuthUser = typeof authUsers.$inferSelect
|
||||
export type NewAuthUser = typeof authUsers.$inferInsert
|
||||
52
db/schema/bankaccounts.ts
Normal file
52
db/schema/bankaccounts.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
doublePrecision,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const bankaccounts = pgTable("bankaccounts", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name"),
|
||||
iban: text("iban").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
bankId: text("bankId").notNull(),
|
||||
ownerName: text("ownerName"),
|
||||
|
||||
accountId: text("accountId").notNull(),
|
||||
|
||||
balance: doublePrecision("balance"),
|
||||
|
||||
expired: boolean("expired").notNull().default(false),
|
||||
|
||||
datevNumber: text("datevNumber"),
|
||||
|
||||
syncedAt: timestamp("synced_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type BankAccount = typeof bankaccounts.$inferSelect
|
||||
export type NewBankAccount = typeof bankaccounts.$inferInsert
|
||||
30
db/schema/bankrequisitions.ts
Normal file
30
db/schema/bankrequisitions.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const bankrequisitions = pgTable("bankrequisitions", {
|
||||
id: uuid("id").primaryKey(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
institutionId: text("institutionId"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||
|
||||
status: text("status"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type BankRequisition = typeof bankrequisitions.$inferSelect
|
||||
export type NewBankRequisition = typeof bankrequisitions.$inferInsert
|
||||
70
db/schema/bankstatements.ts
Normal file
70
db/schema/bankstatements.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
doublePrecision,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { bankaccounts } from "./bankaccounts"
|
||||
import { createddocuments } from "./createddocuments"
|
||||
import { tenants } from "./tenants"
|
||||
import { incominginvoices } from "./incominginvoices"
|
||||
import { contracts } from "./contracts"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const bankstatements = pgTable("bankstatements", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
account: bigint("account", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => bankaccounts.id),
|
||||
|
||||
date: text("date").notNull(),
|
||||
|
||||
credIban: text("credIban"),
|
||||
credName: text("credName"),
|
||||
|
||||
text: text("text"),
|
||||
amount: doublePrecision("amount").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
debIban: text("debIban"),
|
||||
debName: text("debName"),
|
||||
gocardlessId: text("gocardlessId"),
|
||||
currency: text("currency"),
|
||||
valueDate: text("valueDate"),
|
||||
|
||||
incomingInvoice: bigint("incomingInvoice", { mode: "number" }).references(
|
||||
() => incominginvoices.id
|
||||
),
|
||||
|
||||
mandateId: text("mandateId"),
|
||||
|
||||
contract: bigint("contract", { mode: "number" }).references(
|
||||
() => contracts.id
|
||||
),
|
||||
|
||||
createdDocument: bigint("createdDocument", { mode: "number" }).references(
|
||||
() => createddocuments.id
|
||||
),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type BankStatement = typeof bankstatements.$inferSelect
|
||||
export type NewBankStatement = typeof bankstatements.$inferInsert
|
||||
27
db/schema/checkexecutions.ts
Normal file
27
db/schema/checkexecutions.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { checks } from "./checks"
|
||||
|
||||
export const checkexecutions = pgTable("checkexecutions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
check: uuid("check").references(() => checks.id),
|
||||
|
||||
executedAt: timestamp("executed_at"),
|
||||
|
||||
// ❌ executed_by removed (was 0_profiles)
|
||||
|
||||
description: text("description"),
|
||||
})
|
||||
|
||||
export type CheckExecution = typeof checkexecutions.$inferSelect
|
||||
export type NewCheckExecution = typeof checkexecutions.$inferInsert
|
||||
52
db/schema/checks.ts
Normal file
52
db/schema/checks.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
bigint,
|
||||
boolean,
|
||||
jsonb,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { vehicles } from "./vehicles"
|
||||
import { inventoryitems } from "./inventoryitems"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const checks = pgTable("checks", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
vehicle: bigint("vehicle", { mode: "number" })
|
||||
.references(() => vehicles.id),
|
||||
|
||||
// ❌ profile removed (old 0_profiles reference)
|
||||
|
||||
inventoryItem: bigint("inventoryitem", { mode: "number" })
|
||||
.references(() => inventoryitems.id),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name"),
|
||||
type: text("type"),
|
||||
|
||||
distance: bigint("distance", { mode: "number" }).default(1),
|
||||
|
||||
distanceUnit: text("distanceUnit").default("days"),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Check = typeof checks.$inferSelect
|
||||
export type NewCheck = typeof checks.$inferInsert
|
||||
32
db/schema/citys.ts
Normal file
32
db/schema/citys.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
jsonb,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
export const citys = pgTable("citys", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
name: text("name"),
|
||||
short: text("short"),
|
||||
long: text("long"),
|
||||
|
||||
geometry: jsonb("geometry"),
|
||||
|
||||
zip: bigint("zip", { mode: "number" }),
|
||||
|
||||
districtCode: bigint("districtCode", { mode: "number" }),
|
||||
|
||||
countryName: text("countryName"),
|
||||
countryCode: bigint("countryCode", { mode: "number" }),
|
||||
|
||||
districtName: text("districtName"),
|
||||
|
||||
geopoint: text("geopoint"),
|
||||
})
|
||||
|
||||
export type City = typeof citys.$inferSelect
|
||||
export type NewCity = typeof citys.$inferInsert
|
||||
66
db/schema/contacts.ts
Normal file
66
db/schema/contacts.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
date,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { customers } from "./customers"
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const contacts = pgTable(
|
||||
"contacts",
|
||||
{
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
firstName: text("firstName"),
|
||||
lastName: text("lastName"),
|
||||
email: text("email"),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||
|
||||
phoneMobile: text("phoneMobile"),
|
||||
phoneHome: text("phoneHome"),
|
||||
|
||||
heroId: text("heroId"),
|
||||
role: text("role"),
|
||||
|
||||
fullName: text("fullName"),
|
||||
|
||||
salutation: text("salutation"),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }), // vendors folgt separat
|
||||
|
||||
active: boolean("active").notNull().default(true),
|
||||
|
||||
birthday: date("birthday"),
|
||||
notes: text("notes"),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
title: text("title"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
}
|
||||
)
|
||||
|
||||
export type Contact = typeof contacts.$inferSelect
|
||||
export type NewContact = typeof contacts.$inferInsert
|
||||
76
db/schema/contracts.ts
Normal file
76
db/schema/contracts.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { contacts } from "./contacts"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const contracts = pgTable(
|
||||
"contracts",
|
||||
{
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
customer: bigint("customer", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => customers.id),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
active: boolean("active").notNull().default(true),
|
||||
recurring: boolean("recurring").notNull().default(false),
|
||||
|
||||
rhythm: jsonb("rhythm"),
|
||||
|
||||
startDate: timestamp("startDate", { withTimezone: true }),
|
||||
endDate: timestamp("endDate", { withTimezone: true }),
|
||||
signDate: timestamp("signDate", { withTimezone: true }),
|
||||
|
||||
duration: text("duration"),
|
||||
|
||||
contact: bigint("contact", { mode: "number" }).references(
|
||||
() => contacts.id
|
||||
),
|
||||
|
||||
bankingIban: text("bankingIban"),
|
||||
bankingBIC: text("bankingBIC"),
|
||||
bankingName: text("bankingName"),
|
||||
bankingOwner: text("bankingOwner"),
|
||||
sepaRef: text("sepaRef"),
|
||||
sepaDate: timestamp("sepaDate", { withTimezone: true }),
|
||||
|
||||
paymentType: text("paymentType"),
|
||||
invoiceDispatch: text("invoiceDispatch"),
|
||||
|
||||
ownFields: jsonb("ownFields").notNull().default({}),
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
contractNumber: text("contractNumber"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
}
|
||||
)
|
||||
|
||||
export type Contract = typeof contracts.$inferSelect
|
||||
export type NewContract = typeof contracts.$inferInsert
|
||||
50
db/schema/costcentres.ts
Normal file
50
db/schema/costcentres.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { inventoryitems } from "./inventoryitems"
|
||||
import { projects } from "./projects"
|
||||
import { vehicles } from "./vehicles"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const costcentres = pgTable("costcentres", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
number: text("number").notNull(),
|
||||
name: text("name").notNull(),
|
||||
|
||||
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||
|
||||
project: bigint("project", { mode: "number" }).references(() => projects.id),
|
||||
|
||||
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
|
||||
() => inventoryitems.id
|
||||
),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type CostCentre = typeof costcentres.$inferSelect
|
||||
export type NewCostCentre = typeof costcentres.$inferInsert
|
||||
21
db/schema/countrys.ts
Normal file
21
db/schema/countrys.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
export const countrys = pgTable("countrys", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
})
|
||||
|
||||
export type Country = typeof countrys.$inferSelect
|
||||
export type NewCountry = typeof countrys.$inferInsert
|
||||
121
db/schema/createddocuments.ts
Normal file
121
db/schema/createddocuments.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
smallint,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { contacts } from "./contacts"
|
||||
import { contracts } from "./contracts"
|
||||
import { letterheads } from "./letterheads"
|
||||
import { projects } from "./projects"
|
||||
import { plants } from "./plants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const createddocuments = pgTable("createddocuments", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
type: text("type").notNull().default("INVOICE"),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
contact: bigint("contact", { mode: "number" }).references(
|
||||
() => contacts.id
|
||||
),
|
||||
|
||||
address: jsonb("address"),
|
||||
project: bigint("project", { mode: "number" }).references(
|
||||
() => projects.id
|
||||
),
|
||||
|
||||
documentNumber: text("documentNumber"),
|
||||
documentDate: text("documentDate"),
|
||||
|
||||
state: text("state").notNull().default("Entwurf"),
|
||||
|
||||
info: jsonb("info"),
|
||||
|
||||
createdBy: uuid("createdBy").references(() => authUsers.id),
|
||||
|
||||
title: text("title"),
|
||||
description: text("description"),
|
||||
|
||||
startText: text("startText"),
|
||||
endText: text("endText"),
|
||||
|
||||
rows: jsonb("rows").default([]),
|
||||
|
||||
deliveryDateType: text("deliveryDateType"),
|
||||
paymentDays: smallint("paymentDays"),
|
||||
deliveryDate: text("deliveryDate"),
|
||||
|
||||
contactPerson: uuid("contactPerson"),
|
||||
|
||||
serialConfig: jsonb("serialConfig").default({}),
|
||||
|
||||
linkedDocument: bigint("linkedDocument", { mode: "number" }).references(
|
||||
() => createddocuments.id
|
||||
),
|
||||
|
||||
agriculture: jsonb("agriculture"),
|
||||
|
||||
letterhead: bigint("letterhead", { mode: "number" }).references(
|
||||
() => letterheads.id
|
||||
),
|
||||
|
||||
advanceInvoiceResolved: boolean("advanceInvoiceResolved")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
usedAdvanceInvoices: jsonb("usedAdvanceInvoices").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
deliveryDateEnd: text("deliveryDateEnd"),
|
||||
|
||||
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
|
||||
|
||||
taxType: text("taxType"),
|
||||
|
||||
customSurchargePercentage: smallint("customSurchargePercentage")
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
report: jsonb("report").notNull().default({}),
|
||||
|
||||
availableInPortal: boolean("availableInPortal")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
created_by: uuid("created_by").references(() => authUsers.id),
|
||||
|
||||
payment_type: text("payment_type").default("transfer"),
|
||||
|
||||
contract: bigint("contract", { mode: "number" }).references(
|
||||
() => contracts.id
|
||||
),
|
||||
})
|
||||
|
||||
export type CreatedDocument = typeof createddocuments.$inferSelect
|
||||
export type NewCreatedDocument = typeof createddocuments.$inferInsert
|
||||
43
db/schema/createdletters.ts
Normal file
43
db/schema/createdletters.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
bigint,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { vendors } from "./vendors"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const createdletters = pgTable("createdletters", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
contentJson: jsonb("content_json").default([]),
|
||||
|
||||
contentText: text("content_text"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type CreatedLetter = typeof createdletters.$inferSelect
|
||||
export type NewCreatedLetter = typeof createdletters.$inferInsert
|
||||
69
db/schema/customers.ts
Normal file
69
db/schema/customers.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
smallint,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const customers = pgTable(
|
||||
"customers",
|
||||
{
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
customerNumber: text("customerNumber").notNull(),
|
||||
name: text("name").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||
|
||||
infoData: jsonb("infoData").default({}),
|
||||
active: boolean("active").notNull().default(true),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
type: text("type").default("Privat"),
|
||||
heroId: text("heroId"),
|
||||
|
||||
isCompany: boolean("isCompany").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
customPaymentDays: smallint("customPaymentDays"),
|
||||
|
||||
firstname: text("firstname"),
|
||||
lastname: text("lastname"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
customSurchargePercentage: smallint("customSurchargePercentage")
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
salutation: text("salutation"),
|
||||
title: text("title"),
|
||||
nameAddition: text("nameAddition"),
|
||||
|
||||
availableInPortal: boolean("availableInPortal")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
customPaymentType: text("custom_payment_type"), // ENUM payment_types separat?
|
||||
}
|
||||
)
|
||||
|
||||
export type Customer = typeof customers.$inferSelect
|
||||
export type NewCustomer = typeof customers.$inferInsert
|
||||
29
db/schema/devices.ts
Normal file
29
db/schema/devices.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
|
||||
export const devices = pgTable("devices", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
|
||||
|
||||
password: text("password"),
|
||||
|
||||
externalId: text("externalId"),
|
||||
})
|
||||
|
||||
export type Device = typeof devices.$inferSelect
|
||||
export type NewDevice = typeof devices.$inferInsert
|
||||
28
db/schema/documentboxes.ts
Normal file
28
db/schema/documentboxes.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { pgTable, uuid, timestamp, text, boolean, bigint } from "drizzle-orm/pg-core"
|
||||
|
||||
import { spaces } from "./spaces"
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const documentboxes = pgTable("documentboxes", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||
|
||||
key: text("key").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type DocumentBox = typeof documentboxes.$inferSelect
|
||||
export type NewDocumentBox = typeof documentboxes.$inferInsert
|
||||
97
db/schema/enums.ts
Normal file
97
db/schema/enums.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { pgEnum } from "drizzle-orm/pg-core"
|
||||
|
||||
// public.textTemplatePositions
|
||||
export const textTemplatePositionsEnum = pgEnum("texttemplatepositions", [
|
||||
"startText",
|
||||
"endText",
|
||||
])
|
||||
|
||||
// public.folderFunctions
|
||||
export const folderFunctionsEnum = pgEnum("folderfunctions", [
|
||||
"none",
|
||||
"yearSubCategory",
|
||||
"incomingInvoices",
|
||||
"invoices",
|
||||
"quotes",
|
||||
"confirmationOrders",
|
||||
"deliveryNotes",
|
||||
"vehicleData",
|
||||
"reminders",
|
||||
"taxData",
|
||||
"deposit",
|
||||
"timeEvaluations",
|
||||
])
|
||||
|
||||
// public.locked_tenant
|
||||
export const lockedTenantEnum = pgEnum("locked_tenant", [
|
||||
"maintenance_tenant",
|
||||
"maintenance",
|
||||
"general",
|
||||
"no_subscription",
|
||||
])
|
||||
|
||||
// public.credential_types
|
||||
export const credentialTypesEnum = pgEnum("credential_types", [
|
||||
"mail",
|
||||
"m365",
|
||||
])
|
||||
|
||||
// public.payment_types
|
||||
export const paymentTypesEnum = pgEnum("payment_types", [
|
||||
"transfer",
|
||||
"direct_debit",
|
||||
])
|
||||
|
||||
// public.notification_status
|
||||
export const notificationStatusEnum = pgEnum("notification_status", [
|
||||
"queued",
|
||||
"sent",
|
||||
"failed",
|
||||
"read",
|
||||
])
|
||||
|
||||
// public.notification_channel
|
||||
export const notificationChannelEnum = pgEnum("notification_channel", [
|
||||
"email",
|
||||
"inapp",
|
||||
"sms",
|
||||
"push",
|
||||
"webhook",
|
||||
])
|
||||
|
||||
// public.notification_severity
|
||||
export const notificationSeverityEnum = pgEnum("notification_severity", [
|
||||
"info",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
])
|
||||
|
||||
// public.times_state
|
||||
export const timesStateEnum = pgEnum("times_state", [
|
||||
"submitted",
|
||||
"approved",
|
||||
"draft",
|
||||
])
|
||||
|
||||
export const helpdeskStatusEnum = [
|
||||
"open",
|
||||
"in_progress",
|
||||
"waiting_for_customer",
|
||||
"answered",
|
||||
"closed",
|
||||
] as const
|
||||
|
||||
export const helpdeskPriorityEnum = [
|
||||
"low",
|
||||
"normal",
|
||||
"high",
|
||||
] as const
|
||||
|
||||
export const helpdeskDirectionEnum = [
|
||||
"incoming",
|
||||
"outgoing",
|
||||
"internal",
|
||||
"system",
|
||||
] as const
|
||||
|
||||
60
db/schema/events.ts
Normal file
60
db/schema/events.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const events = pgTable(
|
||||
"events",
|
||||
{
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).notNull(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
startDate: timestamp("startDate", { withTimezone: true }).notNull(),
|
||||
endDate: timestamp("endDate", { withTimezone: true }),
|
||||
|
||||
eventtype: text("eventtype").default("Umsetzung"),
|
||||
|
||||
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists
|
||||
|
||||
resources: jsonb("resources").default([]),
|
||||
notes: text("notes"),
|
||||
link: text("link"),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
vehicles: jsonb("vehicles").notNull().default([]),
|
||||
inventoryitems: jsonb("inventoryitems").notNull().default([]),
|
||||
inventoryitemgroups: jsonb("inventoryitemgroups").notNull().default([]),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }), // will link once vendors.ts is created
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
}
|
||||
)
|
||||
|
||||
export type Event = typeof events.$inferSelect
|
||||
export type NewEvent = typeof events.$inferInsert
|
||||
79
db/schema/files.ts
Normal file
79
db/schema/files.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { projects } from "./projects"
|
||||
import { customers } from "./customers"
|
||||
import { contracts } from "./contracts"
|
||||
import { vendors } from "./vendors"
|
||||
import { incominginvoices } from "./incominginvoices"
|
||||
import { plants } from "./plants"
|
||||
import { createddocuments } from "./createddocuments"
|
||||
import { vehicles } from "./vehicles"
|
||||
import { products } from "./products"
|
||||
import { inventoryitems } from "./inventoryitems"
|
||||
import { folders } from "./folders"
|
||||
import { filetags } from "./filetags"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { authProfiles } from "./auth_profiles"
|
||||
import { spaces } from "./spaces"
|
||||
import { documentboxes } from "./documentboxes"
|
||||
import { checks } from "./checks"
|
||||
|
||||
export const files = pgTable("files", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
path: text("path"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
project: bigint("project", { mode: "number" }).references(() => projects.id),
|
||||
customer: bigint("customer", { mode: "number" }).references(() => customers.id),
|
||||
contract: bigint("contract", { mode: "number" }).references(() => contracts.id),
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
incominginvoice: bigint("incominginvoice", { mode: "number" }).references(() => incominginvoices.id),
|
||||
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
|
||||
createddocument: bigint("createddocument", { mode: "number" }).references(() => createddocuments.id),
|
||||
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||
product: bigint("product", { mode: "number" }).references(() => products.id),
|
||||
|
||||
check: uuid("check").references(() => checks.id),
|
||||
|
||||
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(() => inventoryitems.id),
|
||||
|
||||
folder: uuid("folder").references(() => folders.id),
|
||||
|
||||
mimeType: text("mimeType"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||
|
||||
type: uuid("type").references(() => filetags.id),
|
||||
|
||||
documentbox: uuid("documentbox").references(() => documentboxes.id),
|
||||
|
||||
name: text("name"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
|
||||
authProfile: uuid("auth_profile").references(() => authProfiles.id),
|
||||
})
|
||||
|
||||
export type File = typeof files.$inferSelect
|
||||
export type NewFile = typeof files.$inferInsert
|
||||
33
db/schema/filetags.ts
Normal file
33
db/schema/filetags.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
|
||||
export const filetags = pgTable("filetags", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
color: text("color"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
createdDocumentType: text("createddocumenttype").default(""),
|
||||
incomingDocumentType: text("incomingDocumentType"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type FileTag = typeof filetags.$inferSelect
|
||||
export type NewFileTag = typeof filetags.$inferInsert
|
||||
51
db/schema/folders.ts
Normal file
51
db/schema/folders.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
integer,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { filetags } from "./filetags"
|
||||
import { folderFunctionsEnum } from "./enums"
|
||||
|
||||
export const folders = pgTable("folders", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
icon: text("icon"),
|
||||
|
||||
parent: uuid("parent").references(() => folders.id),
|
||||
|
||||
isSystemUsed: boolean("isSystemUsed").notNull().default(false),
|
||||
|
||||
function: folderFunctionsEnum("function"),
|
||||
|
||||
year: integer("year"),
|
||||
|
||||
standardFiletype: uuid("standardFiletype").references(() => filetags.id),
|
||||
|
||||
standardFiletypeIsOptional: boolean("standardFiletypeIsOptional")
|
||||
.notNull()
|
||||
.default(true),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type Folder = typeof folders.$inferSelect
|
||||
export type NewFolder = typeof folders.$inferInsert
|
||||
35
db/schema/generatedexports.ts
Normal file
35
db/schema/generatedexports.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
|
||||
export const generatedexports = pgTable("exports", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
startDate: timestamp("start_date", { withTimezone: true }).notNull(),
|
||||
endDate: timestamp("end_date", { withTimezone: true }).notNull(),
|
||||
|
||||
validUntil: timestamp("valid_until", { withTimezone: true }),
|
||||
|
||||
type: text("type").notNull().default("datev"),
|
||||
|
||||
url: text("url").notNull(),
|
||||
filePath: text("file_path"),
|
||||
})
|
||||
|
||||
export type Export = typeof generatedexports.$inferSelect
|
||||
export type NewExport = typeof generatedexports.$inferInsert
|
||||
22
db/schema/globalmessages.ts
Normal file
22
db/schema/globalmessages.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
export const globalmessages = pgTable("globalmessages", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
title: text("title"),
|
||||
description: text("description"),
|
||||
})
|
||||
|
||||
export type GlobalMessage = typeof globalmessages.$inferSelect
|
||||
export type NewGlobalMessage = typeof globalmessages.$inferInsert
|
||||
17
db/schema/globalmessagesseen.ts
Normal file
17
db/schema/globalmessagesseen.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
pgTable,
|
||||
timestamp,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { globalmessages } from "./globalmessages"
|
||||
|
||||
export const globalmessagesseen = pgTable("globalmessagesseen", {
|
||||
message: bigint("message", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => globalmessages.id),
|
||||
|
||||
seenAt: timestamp("seen_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
})
|
||||
44
db/schema/helpdesk_channel_instances.ts
Normal file
44
db/schema/helpdesk_channel_instances.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { helpdesk_channel_types } from "./helpdesk_channel_types"
|
||||
|
||||
export const helpdesk_channel_instances = pgTable("helpdesk_channel_instances", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
typeId: text("type_id")
|
||||
.notNull()
|
||||
.references(() => helpdesk_channel_types.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
|
||||
config: jsonb("config").notNull(),
|
||||
publicConfig: jsonb("public_config").notNull().default({}),
|
||||
|
||||
publicToken: text("public_token").unique(),
|
||||
secretToken: text("secret_token"),
|
||||
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type HelpdeskChannelInstance =
|
||||
typeof helpdesk_channel_instances.$inferSelect
|
||||
export type NewHelpdeskChannelInstance =
|
||||
typeof helpdesk_channel_instances.$inferInsert
|
||||
9
db/schema/helpdesk_channel_types.ts
Normal file
9
db/schema/helpdesk_channel_types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { pgTable, text } from "drizzle-orm/pg-core"
|
||||
|
||||
export const helpdesk_channel_types = pgTable("helpdesk_channel_types", {
|
||||
id: text("id").primaryKey(),
|
||||
description: text("description").notNull(),
|
||||
})
|
||||
|
||||
export type HelpdeskChannelType = typeof helpdesk_channel_types.$inferSelect
|
||||
export type NewHelpdeskChannelType = typeof helpdesk_channel_types.$inferInsert
|
||||
45
db/schema/helpdesk_contacts.ts
Normal file
45
db/schema/helpdesk_contacts.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { contacts } from "./contacts"
|
||||
import { helpdesk_channel_instances } from "./helpdesk_channel_instances" // placeholder
|
||||
|
||||
export const helpdesk_contacts = pgTable("helpdesk_contacts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
customerId: bigint("customer_id", { mode: "number" })
|
||||
.references(() => customers.id, { onDelete: "set null" }),
|
||||
|
||||
email: text("email"),
|
||||
phone: text("phone"),
|
||||
|
||||
externalRef: jsonb("external_ref"),
|
||||
displayName: text("display_name"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
sourceChannelId: uuid("source_channel_id").references(
|
||||
() => helpdesk_channel_instances.id,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
|
||||
contactId: bigint("contact_id", { mode: "number" }).references(
|
||||
() => contacts.id,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
})
|
||||
|
||||
export type HelpdeskContact = typeof helpdesk_contacts.$inferSelect
|
||||
export type NewHelpdeskContact = typeof helpdesk_contacts.$inferInsert
|
||||
34
db/schema/helpdesk_conversation_participants.ts
Normal file
34
db/schema/helpdesk_conversation_participants.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { helpdesk_conversations } from "./helpdesk_conversations"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const helpdesk_conversation_participants = pgTable(
|
||||
"helpdesk_conversation_participants",
|
||||
{
|
||||
conversationId: uuid("conversation_id")
|
||||
.notNull()
|
||||
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
|
||||
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||
|
||||
role: text("role"),
|
||||
},
|
||||
(table) => ({
|
||||
pk: {
|
||||
name: "helpdesk_conversation_participants_pkey",
|
||||
columns: [table.conversationId, table.userId],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export type HelpdeskConversationParticipant =
|
||||
typeof helpdesk_conversation_participants.$inferSelect
|
||||
export type NewHelpdeskConversationParticipant =
|
||||
typeof helpdesk_conversation_participants.$inferInsert
|
||||
59
db/schema/helpdesk_conversations.ts
Normal file
59
db/schema/helpdesk_conversations.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { helpdesk_contacts } from "./helpdesk_contacts"
|
||||
import { contacts } from "./contacts"
|
||||
import { customers } from "./customers"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { helpdesk_channel_instances } from "./helpdesk_channel_instances"
|
||||
|
||||
export const helpdesk_conversations = pgTable("helpdesk_conversations", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
channelInstanceId: uuid("channel_instance_id")
|
||||
.notNull()
|
||||
.references(() => helpdesk_channel_instances.id, { onDelete: "cascade" }),
|
||||
|
||||
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
subject: text("subject"),
|
||||
|
||||
status: text("status").notNull().default("open"),
|
||||
|
||||
priority: text("priority").default("normal"),
|
||||
|
||||
assigneeUserId: uuid("assignee_user_id").references(() => authUsers.id),
|
||||
|
||||
lastMessageAt: timestamp("last_message_at", { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
customerId: bigint("customer_id", { mode: "number" }).references(
|
||||
() => customers.id,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
|
||||
contactPersonId: bigint("contact_person_id", { mode: "number" }).references(
|
||||
() => contacts.id,
|
||||
{ onDelete: "set null" }
|
||||
),
|
||||
|
||||
ticketNumber: text("ticket_number"),
|
||||
})
|
||||
|
||||
export type HelpdeskConversation =
|
||||
typeof helpdesk_conversations.$inferSelect
|
||||
export type NewHelpdeskConversation =
|
||||
typeof helpdesk_conversations.$inferInsert
|
||||
46
db/schema/helpdesk_messages.ts
Normal file
46
db/schema/helpdesk_messages.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { helpdesk_contacts } from "./helpdesk_contacts"
|
||||
import { helpdesk_conversations } from "./helpdesk_conversations"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const helpdesk_messages = pgTable("helpdesk_messages", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
conversationId: uuid("conversation_id")
|
||||
.notNull()
|
||||
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
|
||||
|
||||
direction: text("direction").notNull(),
|
||||
|
||||
authorUserId: uuid("author_user_id").references(() => authUsers.id),
|
||||
|
||||
payload: jsonb("payload").notNull(),
|
||||
|
||||
rawMeta: jsonb("raw_meta"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
|
||||
externalMessageId: text("external_message_id").unique(),
|
||||
|
||||
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type HelpdeskMessage = typeof helpdesk_messages.$inferSelect
|
||||
export type NewHelpdeskMessage = typeof helpdesk_messages.$inferInsert
|
||||
33
db/schema/helpdesk_routing_rules.ts
Normal file
33
db/schema/helpdesk_routing_rules.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const helpdesk_routing_rules = pgTable("helpdesk_routing_rules", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
condition: jsonb("condition").notNull(),
|
||||
action: jsonb("action").notNull(),
|
||||
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type HelpdeskRoutingRule =
|
||||
typeof helpdesk_routing_rules.$inferSelect
|
||||
export type NewHelpdeskRoutingRule =
|
||||
typeof helpdesk_routing_rules.$inferInsert
|
||||
140
db/schema/historyitems.ts
Normal file
140
db/schema/historyitems.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { vendors } from "./vendors"
|
||||
import { projects } from "./projects"
|
||||
import { plants } from "./plants"
|
||||
import { incominginvoices } from "./incominginvoices"
|
||||
import { contacts } from "./contacts"
|
||||
import { inventoryitems } from "./inventoryitems"
|
||||
import { products } from "./products"
|
||||
import { tasks } from "./tasks"
|
||||
import { vehicles } from "./vehicles"
|
||||
import { bankstatements } from "./bankstatements"
|
||||
import { spaces } from "./spaces"
|
||||
import { costcentres } from "./costcentres"
|
||||
import { ownaccounts } from "./ownaccounts"
|
||||
import { createddocuments } from "./createddocuments"
|
||||
import { documentboxes } from "./documentboxes"
|
||||
import { hourrates } from "./hourrates"
|
||||
import { projecttypes } from "./projecttypes"
|
||||
import { checks } from "./checks"
|
||||
import { services } from "./services"
|
||||
import { events } from "./events"
|
||||
import { inventoryitemgroups } from "./inventoryitemgroups"
|
||||
import { authUsers } from "./auth_users"
|
||||
import {files} from "./files";
|
||||
|
||||
export const historyitems = pgTable("historyitems", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
text: text("text").notNull(),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
project: bigint("project", { mode: "number" }).references(
|
||||
() => projects.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
plant: bigint("plant", { mode: "number" }).references(
|
||||
() => plants.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
incomingInvoice: bigint("incomingInvoice", { mode: "number" }).references(
|
||||
() => incominginvoices.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
contact: bigint("contact", { mode: "number" }).references(() => contacts.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
|
||||
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
|
||||
() => inventoryitems.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
product: bigint("product", { mode: "number" }).references(
|
||||
() => products.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
event: bigint("event", { mode: "number" }).references(() => events.id),
|
||||
|
||||
newVal: text("newVal"),
|
||||
oldVal: text("oldVal"),
|
||||
|
||||
task: bigint("task", { mode: "number" }).references(() => tasks.id),
|
||||
|
||||
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||
|
||||
bankstatement: bigint("bankstatement", { mode: "number" }).references(
|
||||
() => bankstatements.id
|
||||
),
|
||||
|
||||
space: bigint("space", { mode: "number" }).references(() => spaces.id),
|
||||
|
||||
config: jsonb("config"),
|
||||
|
||||
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||
() => projecttypes.id
|
||||
),
|
||||
|
||||
check: uuid("check").references(() => checks.id),
|
||||
|
||||
service: bigint("service", { mode: "number" }).references(
|
||||
() => services.id
|
||||
),
|
||||
|
||||
createddocument: bigint("createddocument", { mode: "number" }).references(
|
||||
() => createddocuments.id
|
||||
),
|
||||
|
||||
file: uuid("file").references(() => files.id),
|
||||
|
||||
inventoryitemgroup: uuid("inventoryitemgroup").references(
|
||||
() => inventoryitemgroups.id
|
||||
),
|
||||
|
||||
source: text("source").default("Software"),
|
||||
|
||||
costcentre: uuid("costcentre").references(() => costcentres.id),
|
||||
|
||||
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
|
||||
|
||||
documentbox: uuid("documentbox").references(() => documentboxes.id),
|
||||
|
||||
hourrate: uuid("hourrate").references(() => hourrates.id),
|
||||
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
|
||||
action: text("action"),
|
||||
})
|
||||
|
||||
export type HistoryItem = typeof historyitems.$inferSelect
|
||||
export type NewHistoryItem = typeof historyitems.$inferInsert
|
||||
18
db/schema/holidays.ts
Normal file
18
db/schema/holidays.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { pgTable, bigint, date, text, timestamp } from "drizzle-orm/pg-core"
|
||||
|
||||
export const holidays = pgTable("holidays", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedAlwaysAsIdentity(),
|
||||
|
||||
date: date("date").notNull(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
stateCode: text("state_code").notNull(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type Holiday = typeof holidays.$inferSelect
|
||||
export type NewHoliday = typeof holidays.$inferInsert
|
||||
27
db/schema/hourrates.ts
Normal file
27
db/schema/hourrates.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { pgTable, uuid, timestamp, text, boolean, bigint, doublePrecision } from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const hourrates = pgTable("hourrates", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
purchasePrice: doublePrecision("purchasePrice").notNull(),
|
||||
sellingPrice: doublePrecision("sellingPrice").notNull(),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type HourRate = typeof hourrates.$inferSelect
|
||||
export type NewHourRate = typeof hourrates.$inferInsert
|
||||
63
db/schema/incominginvoices.ts
Normal file
63
db/schema/incominginvoices.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { vendors } from "./vendors"
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const incominginvoices = pgTable("incominginvoices", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
state: text("state").notNull().default("Entwurf"),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
reference: text("reference"),
|
||||
date: text("date"),
|
||||
|
||||
document: bigint("document", { mode: "number" }),
|
||||
|
||||
dueDate: text("dueDate"),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
paymentType: text("paymentType"),
|
||||
|
||||
accounts: jsonb("accounts").notNull().default([
|
||||
{
|
||||
account: null,
|
||||
taxType: null,
|
||||
amountNet: null,
|
||||
amountTax: 19,
|
||||
costCentre: null,
|
||||
},
|
||||
]),
|
||||
|
||||
paid: boolean("paid").notNull().default(false),
|
||||
expense: boolean("expense").notNull().default(true),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type IncomingInvoice = typeof incominginvoices.$inferSelect
|
||||
export type NewIncomingInvoice = typeof incominginvoices.$inferInsert
|
||||
70
db/schema/index.ts
Normal file
70
db/schema/index.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export * from "./accounts"
|
||||
export * from "./auth_profiles"
|
||||
export * from "./auth_role_permisssions"
|
||||
export * from "./auth_roles"
|
||||
export * from "./auth_tenant_users"
|
||||
export * from "./auth_user_roles"
|
||||
export * from "./auth_users"
|
||||
export * from "./bankaccounts"
|
||||
export * from "./bankrequisitions"
|
||||
export * from "./bankstatements"
|
||||
export * from "./checkexecutions"
|
||||
export * from "./checks"
|
||||
export * from "./citys"
|
||||
export * from "./contacts"
|
||||
export * from "./contracts"
|
||||
export * from "./costcentres"
|
||||
export * from "./countrys"
|
||||
export * from "./createddocuments"
|
||||
export * from "./createdletters"
|
||||
export * from "./customers"
|
||||
export * from "./devices"
|
||||
export * from "./documentboxes"
|
||||
export * from "./enums"
|
||||
export * from "./events"
|
||||
export * from "./files"
|
||||
export * from "./filetags"
|
||||
export * from "./folders"
|
||||
export * from "./generatedexports"
|
||||
export * from "./globalmessages"
|
||||
export * from "./globalmessagesseen"
|
||||
export * from "./helpdesk_channel_instances"
|
||||
export * from "./helpdesk_channel_types"
|
||||
export * from "./helpdesk_contacts"
|
||||
export * from "./helpdesk_conversation_participants"
|
||||
export * from "./helpdesk_conversations"
|
||||
export * from "./helpdesk_messages"
|
||||
export * from "./helpdesk_routing_rules"
|
||||
export * from "./historyitems"
|
||||
export * from "./holidays"
|
||||
export * from "./hourrates"
|
||||
export * from "./incominginvoices"
|
||||
export * from "./inventoryitemgroups"
|
||||
export * from "./inventoryitems"
|
||||
export * from "./letterheads"
|
||||
export * from "./movements"
|
||||
export * from "./notifications_event_types"
|
||||
export * from "./notifications_items"
|
||||
export * from "./notifications_preferences"
|
||||
export * from "./notifications_preferences_defaults"
|
||||
export * from "./ownaccounts"
|
||||
export * from "./plants"
|
||||
export * from "./productcategories"
|
||||
export * from "./products"
|
||||
export * from "./projects"
|
||||
export * from "./projecttypes"
|
||||
export * from "./servicecategories"
|
||||
export * from "./services"
|
||||
export * from "./spaces"
|
||||
export * from "./staff_time_entries"
|
||||
export * from "./staff_time_entry_connects"
|
||||
export * from "./staff_zeitstromtimestamps"
|
||||
export * from "./statementallocations"
|
||||
export * from "./tasks"
|
||||
export * from "./taxtypes"
|
||||
export * from "./tenants"
|
||||
export * from "./texttemplates"
|
||||
export * from "./units"
|
||||
export * from "./user_credentials"
|
||||
export * from "./vehicles"
|
||||
export * from "./vendors"
|
||||
39
db/schema/inventoryitemgroups.ts
Normal file
39
db/schema/inventoryitemgroups.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb, bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const inventoryitemgroups = pgTable("inventoryitemgroups", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" }).notNull().references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
inventoryitems: jsonb("inventoryitems").notNull().default([]),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
usePlanning: boolean("usePlanning").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type InventoryItemGroup = typeof inventoryitemgroups.$inferSelect
|
||||
export type NewInventoryItemGroup = typeof inventoryitemgroups.$inferInsert
|
||||
68
db/schema/inventoryitems.ts
Normal file
68
db/schema/inventoryitems.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
doublePrecision,
|
||||
uuid,
|
||||
jsonb,
|
||||
date,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { vendors } from "./vendors"
|
||||
import { spaces } from "./spaces"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const inventoryitems = pgTable("inventoryitems", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
usePlanning: boolean("usePlanning").notNull().default(false),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
currentSpace: bigint("currentSpace", { mode: "number" }).references(
|
||||
() => spaces.id
|
||||
),
|
||||
|
||||
articleNumber: text("articleNumber"),
|
||||
serialNumber: text("serialNumber"),
|
||||
|
||||
purchaseDate: date("purchaseDate"),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
quantity: bigint("quantity", { mode: "number" }).notNull().default(0),
|
||||
|
||||
purchasePrice: doublePrecision("purchasePrice").default(0),
|
||||
|
||||
manufacturer: text("manufacturer"),
|
||||
manufacturerNumber: text("manufacturerNumber"),
|
||||
|
||||
currentValue: doublePrecision("currentValue"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() =>
|
||||
authUsers.id
|
||||
),
|
||||
})
|
||||
|
||||
export type InventoryItem = typeof inventoryitems.$inferSelect
|
||||
export type NewInventoryItem = typeof inventoryitems.$inferInsert
|
||||
39
db/schema/letterheads.ts
Normal file
39
db/schema/letterheads.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const letterheads = pgTable("letterheads", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").default("Standard"),
|
||||
|
||||
path: text("path").notNull(),
|
||||
|
||||
documentTypes: text("documentTypes").array().notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type Letterhead = typeof letterheads.$inferSelect
|
||||
export type NewLetterhead = typeof letterheads.$inferInsert
|
||||
49
db/schema/movements.ts
Normal file
49
db/schema/movements.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { products } from "./products"
|
||||
import { spaces } from "./spaces"
|
||||
import { tenants } from "./tenants"
|
||||
import { projects } from "./projects"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const movements = pgTable("movements", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
quantity: bigint("quantity", { mode: "number" }).notNull(),
|
||||
|
||||
productId: bigint("productId", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
|
||||
spaceId: bigint("spaceId", { mode: "number" }).references(() => spaces.id),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
projectId: bigint("projectId", { mode: "number" }).references(
|
||||
() => projects.id
|
||||
),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
serials: text("serials").array(),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Movement = typeof movements.$inferSelect
|
||||
export type NewMovement = typeof movements.$inferInsert
|
||||
34
db/schema/notifications_event_types.ts
Normal file
34
db/schema/notifications_event_types.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
pgTable,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core"
|
||||
import {notificationSeverityEnum} from "./enums";
|
||||
|
||||
|
||||
export const notificationsEventTypes = pgTable("notifications_event_types", {
|
||||
eventKey: text("event_key").primaryKey(),
|
||||
|
||||
displayName: text("display_name").notNull(),
|
||||
description: text("description"),
|
||||
category: text("category"),
|
||||
|
||||
severity: notificationSeverityEnum("severity").notNull().default("info"),
|
||||
|
||||
allowedChannels: jsonb("allowed_channels").notNull().default(["inapp", "email"]),
|
||||
|
||||
payloadSchema: jsonb("payload_schema"),
|
||||
|
||||
isActive: boolean("is_active").notNull().default(true),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
})
|
||||
|
||||
export type NotificationsEventType =
|
||||
typeof notificationsEventTypes.$inferSelect
|
||||
export type NewNotificationsEventType =
|
||||
typeof notificationsEventTypes.$inferInsert
|
||||
54
db/schema/notifications_items.ts
Normal file
54
db/schema/notifications_items.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
text,
|
||||
jsonb,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { notificationsEventTypes } from "./notifications_event_types"
|
||||
import {notificationChannelEnum, notificationStatusEnum} from "./enums";
|
||||
|
||||
|
||||
export const notificationsItems = pgTable("notifications_items", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
||||
|
||||
eventType: text("event_type")
|
||||
.notNull()
|
||||
.references(() => notificationsEventTypes.eventKey, {
|
||||
onUpdate: "cascade",
|
||||
onDelete: "restrict",
|
||||
}),
|
||||
|
||||
title: text("title").notNull(),
|
||||
message: text("message").notNull(),
|
||||
|
||||
payload: jsonb("payload"),
|
||||
|
||||
channel: notificationChannelEnum("channel").notNull(),
|
||||
|
||||
status: notificationStatusEnum("status").notNull().default("queued"),
|
||||
|
||||
error: text("error"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
sentAt: timestamp("sent_at", { withTimezone: true }),
|
||||
readAt: timestamp("read_at", { withTimezone: true }),
|
||||
})
|
||||
|
||||
export type NotificationItem = typeof notificationsItems.$inferSelect
|
||||
export type NewNotificationItem = typeof notificationsItems.$inferInsert
|
||||
60
db/schema/notifications_preferences.ts
Normal file
60
db/schema/notifications_preferences.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
text,
|
||||
boolean,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { notificationsEventTypes } from "./notifications_event_types"
|
||||
import {notificationChannelEnum} from "./enums";
|
||||
|
||||
export const notificationsPreferences = pgTable(
|
||||
"notifications_preferences",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
|
||||
eventType: text("event_type")
|
||||
.notNull()
|
||||
.references(() => notificationsEventTypes.eventKey, {
|
||||
onDelete: "restrict",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
|
||||
channel: notificationChannelEnum("channel").notNull(),
|
||||
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
uniquePrefs: uniqueIndex(
|
||||
"notifications_preferences_tenant_id_user_id_event_type_chan_key",
|
||||
).on(table.tenantId, table.userId, table.eventType, table.channel),
|
||||
}),
|
||||
)
|
||||
|
||||
export type NotificationPreference =
|
||||
typeof notificationsPreferences.$inferSelect
|
||||
export type NewNotificationPreference =
|
||||
typeof notificationsPreferences.$inferInsert
|
||||
52
db/schema/notifications_preferences_defaults.ts
Normal file
52
db/schema/notifications_preferences_defaults.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
text,
|
||||
boolean,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { notificationsEventTypes } from "./notifications_event_types"
|
||||
import {notificationChannelEnum} from "./enums";
|
||||
|
||||
export const notificationsPreferencesDefaults = pgTable(
|
||||
"notifications_preferences_defaults",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
|
||||
eventKey: text("event_key")
|
||||
.notNull()
|
||||
.references(() => notificationsEventTypes.eventKey, {
|
||||
onDelete: "restrict",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
|
||||
channel: notificationChannelEnum("channel").notNull(),
|
||||
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
uniqueDefaults: uniqueIndex(
|
||||
"notifications_preferences_defau_tenant_id_event_key_channel_key",
|
||||
).on(table.tenantId, table.eventKey, table.channel),
|
||||
}),
|
||||
)
|
||||
|
||||
export type NotificationPreferenceDefault =
|
||||
typeof notificationsPreferencesDefaults.$inferSelect
|
||||
export type NewNotificationPreferenceDefault =
|
||||
typeof notificationsPreferencesDefaults.$inferInsert
|
||||
39
db/schema/ownaccounts.ts
Normal file
39
db/schema/ownaccounts.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
bigint,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const ownaccounts = pgTable("ownaccounts", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
number: text("number").notNull(),
|
||||
name: text("name").notNull(),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type OwnAccount = typeof ownaccounts.$inferSelect
|
||||
export type NewOwnAccount = typeof ownaccounts.$inferInsert
|
||||
56
db/schema/plants.ts
Normal file
56
db/schema/plants.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
uuid,
|
||||
date,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { contracts } from "./contracts"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const plants = pgTable("plants", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
infoData: jsonb("infoData"),
|
||||
contract: bigint("contract", { mode: "number" }).references(
|
||||
() => contracts.id
|
||||
),
|
||||
|
||||
description: jsonb("description").default({
|
||||
html: "",
|
||||
json: [],
|
||||
text: "",
|
||||
}),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Plant = typeof plants.$inferSelect
|
||||
export type NewPlant = typeof plants.$inferInsert
|
||||
37
db/schema/productcategories.ts
Normal file
37
db/schema/productcategories.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const productcategories = pgTable("productcategories", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type ProductCategory = typeof productcategories.$inferSelect
|
||||
export type NewProductCategory = typeof productcategories.$inferInsert
|
||||
69
db/schema/products.ts
Normal file
69
db/schema/products.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
doublePrecision,
|
||||
boolean,
|
||||
smallint,
|
||||
uuid,
|
||||
jsonb,
|
||||
json,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { units } from "./units"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const products = pgTable("products", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
manufacturer: text("manufacturer"),
|
||||
|
||||
unit: bigint("unit", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => units.id),
|
||||
|
||||
tags: json("tags").notNull().default([]),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
ean: text("ean"),
|
||||
barcode: text("barcode"),
|
||||
|
||||
purchase_price: doublePrecision("purchasePrice"),
|
||||
selling_price: doublePrecision("sellingPrice"),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
manufacturer_number: text("manufacturerNumber"),
|
||||
|
||||
vendor_allocation: jsonb("vendorAllocation").default([]),
|
||||
|
||||
article_number: text("articleNumber"),
|
||||
|
||||
barcodes: text("barcodes").array().notNull().default([]),
|
||||
|
||||
productcategories: jsonb("productcategories").default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
tax_percentage: smallint("taxPercentage").notNull().default(19),
|
||||
|
||||
markup_percentage: doublePrecision("markupPercentage"),
|
||||
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Product = typeof products.$inferSelect
|
||||
export type NewProduct = typeof products.$inferInsert
|
||||
78
db/schema/projects.ts
Normal file
78
db/schema/projects.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
json,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { customers } from "./customers"
|
||||
import { contracts } from "./contracts"
|
||||
import { projecttypes } from "./projecttypes"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const projects = pgTable("projects", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
phases: jsonb("phases").default([]),
|
||||
|
||||
description: json("description"),
|
||||
|
||||
forms: jsonb("forms").default([]),
|
||||
|
||||
heroId: text("heroId"),
|
||||
|
||||
measure: text("measure"),
|
||||
|
||||
material: jsonb("material"),
|
||||
|
||||
plant: bigint("plant", { mode: "number" }),
|
||||
|
||||
profiles: uuid("profiles").array().notNull().default([]),
|
||||
|
||||
projectNumber: text("projectNumber"),
|
||||
|
||||
contract: bigint("contract", { mode: "number" }).references(
|
||||
() => contracts.id
|
||||
),
|
||||
|
||||
projectType: text("projectType").default("Projekt"),
|
||||
|
||||
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||
() => projecttypes.id
|
||||
),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
customerRef: text("customerRef"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
active_phase: text("active_phase"),
|
||||
})
|
||||
|
||||
export type Project = typeof projects.$inferSelect
|
||||
export type NewProject = typeof projects.$inferInsert
|
||||
41
db/schema/projecttypes.ts
Normal file
41
db/schema/projecttypes.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const projecttypes = pgTable("projecttypes", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
initialPhases: jsonb("initialPhases"),
|
||||
addablePhases: jsonb("addablePhases"),
|
||||
|
||||
icon: text("icon"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type ProjectType = typeof projecttypes.$inferSelect
|
||||
export type NewProjectType = typeof projecttypes.$inferInsert
|
||||
39
db/schema/servicecategories.ts
Normal file
39
db/schema/servicecategories.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
doublePrecision,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const servicecategories = pgTable("servicecategories", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
|
||||
discount: doublePrecision("discount").default(0),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type ServiceCategory = typeof servicecategories.$inferSelect
|
||||
export type NewServiceCategory = typeof servicecategories.$inferInsert
|
||||
63
db/schema/services.ts
Normal file
63
db/schema/services.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
doublePrecision,
|
||||
jsonb,
|
||||
boolean,
|
||||
smallint,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { units } from "./units"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const services = pgTable("services", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
sellingPrice: doublePrecision("sellingPrice"),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
unit: bigint("unit", { mode: "number" }).references(() => units.id),
|
||||
|
||||
serviceNumber: bigint("serviceNumber", { mode: "number" }),
|
||||
|
||||
tags: jsonb("tags").default([]),
|
||||
servicecategories: jsonb("servicecategories").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
purchasePriceComposed: jsonb("purchasePriceComposed")
|
||||
.notNull()
|
||||
.default({ total: 0 }),
|
||||
|
||||
sellingPriceComposed: jsonb("sellingPriceComposed")
|
||||
.notNull()
|
||||
.default({ total: 0 }),
|
||||
|
||||
taxPercentage: smallint("taxPercentage").notNull().default(19),
|
||||
|
||||
materialComposition: jsonb("materialComposition").notNull().default([]),
|
||||
personalComposition: jsonb("personalComposition").notNull().default([]),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Service = typeof services.$inferSelect
|
||||
export type NewService = typeof services.$inferInsert
|
||||
49
db/schema/spaces.ts
Normal file
49
db/schema/spaces.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const spaces = pgTable("spaces", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name"),
|
||||
type: text("type").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
space_number: text("spaceNumber").notNull(),
|
||||
|
||||
parentSpace: bigint("parentSpace", { mode: "number" }).references(
|
||||
() => spaces.id
|
||||
),
|
||||
|
||||
info_data: jsonb("infoData")
|
||||
.notNull()
|
||||
.default({ zip: "", city: "", streetNumber: "" }),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Space = typeof spaces.$inferSelect
|
||||
export type NewSpace = typeof spaces.$inferInsert
|
||||
68
db/schema/staff_time_entries.ts
Normal file
68
db/schema/staff_time_entries.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
timestamp,
|
||||
integer,
|
||||
text,
|
||||
boolean,
|
||||
numeric,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { timesStateEnum } from "./enums"
|
||||
import {sql} from "drizzle-orm";
|
||||
|
||||
export const stafftimeentries = pgTable("staff_time_entries", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stoppedAt: timestamp("stopped_at", { withTimezone: true }),
|
||||
|
||||
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
sql`CASE
|
||||
WHEN stopped_at IS NOT NULL
|
||||
THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)
|
||||
ELSE NULL
|
||||
END`
|
||||
),
|
||||
|
||||
type: text("type").default("work"),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
source: text("source"),
|
||||
|
||||
state: timesStateEnum("state").notNull().default("draft"),
|
||||
|
||||
device: uuid("device"),
|
||||
|
||||
internalNote: text("internal_note"),
|
||||
|
||||
vacationReason: text("vacation_reason"),
|
||||
vacationDays: numeric("vacation_days", { precision: 5, scale: 2 }),
|
||||
|
||||
approvedBy: uuid("approved_by").references(() => authUsers.id),
|
||||
approvedAt: timestamp("approved_at", { withTimezone: true }),
|
||||
|
||||
sickReason: text("sick_reason"),
|
||||
})
|
||||
|
||||
export type StaffTimeEntry = typeof stafftimeentries.$inferSelect
|
||||
export type NewStaffTimeEntry = typeof stafftimeentries.$inferInsert
|
||||
38
db/schema/staff_time_entry_connects.ts
Normal file
38
db/schema/staff_time_entry_connects.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
timestamp,
|
||||
integer,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { stafftimeentries } from "./staff_time_entries"
|
||||
import {sql} from "drizzle-orm";
|
||||
|
||||
export const staffTimeEntryConnects = pgTable("staff_time_entry_connects", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
timeEntryId: uuid("time_entry_id")
|
||||
.notNull()
|
||||
.references(() => stafftimeentries.id, { onDelete: "cascade" }),
|
||||
|
||||
projectId: bigint("project_id", { mode: "number" }), // referenziert später projects.id
|
||||
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stoppedAt: timestamp("stopped_at", { withTimezone: true }).notNull(),
|
||||
|
||||
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
sql`(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)`
|
||||
),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type StaffTimeEntryConnect =
|
||||
typeof staffTimeEntryConnects.$inferSelect
|
||||
export type NewStaffTimeEntryConnect =
|
||||
typeof staffTimeEntryConnects.$inferInsert
|
||||
44
db/schema/staff_zeitstromtimestamps.ts
Normal file
44
db/schema/staff_zeitstromtimestamps.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
bigint,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authProfiles } from "./auth_profiles"
|
||||
import { stafftimeentries } from "./staff_time_entries"
|
||||
|
||||
export const staffZeitstromTimestamps = pgTable("staff_zeitstromtimestamps", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
profile: uuid("profile")
|
||||
.notNull()
|
||||
.references(() => authProfiles.id),
|
||||
|
||||
key: text("key").notNull(),
|
||||
|
||||
intent: text("intent").notNull(),
|
||||
|
||||
time: timestamp("time", { withTimezone: true }).notNull(),
|
||||
|
||||
staffTimeEntry: uuid("staff_time_entry").references(
|
||||
() => stafftimeentries.id
|
||||
),
|
||||
|
||||
internalNote: text("internal_note"),
|
||||
})
|
||||
|
||||
export type StaffZeitstromTimestamp =
|
||||
typeof staffZeitstromTimestamps.$inferSelect
|
||||
export type NewStaffZeitstromTimestamp =
|
||||
typeof staffZeitstromTimestamps.$inferInsert
|
||||
69
db/schema/statementallocations.ts
Normal file
69
db/schema/statementallocations.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
integer,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
doublePrecision,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { customers } from "./customers"
|
||||
import { vendors } from "./vendors"
|
||||
import { ownaccounts } from "./ownaccounts"
|
||||
import { incominginvoices } from "./incominginvoices"
|
||||
import { createddocuments } from "./createddocuments"
|
||||
import { bankstatements } from "./bankstatements"
|
||||
import { accounts } from "./accounts" // Falls noch nicht erstellt → bitte melden!
|
||||
|
||||
export const statementallocations = pgTable("statementallocations", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
// foreign keys
|
||||
bs_id: integer("bs_id")
|
||||
.notNull()
|
||||
.references(() => bankstatements.id),
|
||||
|
||||
cd_id: integer("cd_id").references(() => createddocuments.id),
|
||||
|
||||
amount: doublePrecision("amount").notNull().default(0),
|
||||
|
||||
ii_id: bigint("ii_id", { mode: "number" }).references(
|
||||
() => incominginvoices.id
|
||||
),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
account: bigint("account", { mode: "number" }).references(
|
||||
() => accounts.id
|
||||
),
|
||||
|
||||
createdAt: timestamp("created_at", {
|
||||
withTimezone: false,
|
||||
}).defaultNow(),
|
||||
|
||||
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
|
||||
|
||||
description: text("description"),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
})
|
||||
|
||||
export type StatementAllocation = typeof statementallocations.$inferSelect
|
||||
export type NewStatementAllocation =
|
||||
typeof statementallocations.$inferInsert
|
||||
51
db/schema/tasks.ts
Normal file
51
db/schema/tasks.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { customers } from "./customers"
|
||||
|
||||
export const tasks = pgTable("tasks", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
categorie: text("categorie"),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
// FIXED: user_id statt profile, verweist auf auth_users.id
|
||||
userId: uuid("user_id").references(() => authUsers.id),
|
||||
|
||||
project: bigint("project", { mode: "number" }),
|
||||
plant: bigint("plant", { mode: "number" }),
|
||||
|
||||
customer: bigint("customer", { mode: "number" }).references(
|
||||
() => customers.id
|
||||
),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Task = typeof tasks.$inferSelect
|
||||
export type NewTask = typeof tasks.$inferInsert
|
||||
28
db/schema/taxtypes.ts
Normal file
28
db/schema/taxtypes.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const taxTypes = pgTable("taxtypes", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
label: text("label").notNull(),
|
||||
percentage: bigint("percentage", { mode: "number" }).notNull(),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type TaxType = typeof taxTypes.$inferSelect
|
||||
export type NewTaxType = typeof taxTypes.$inferInsert
|
||||
140
db/schema/tenants.ts
Normal file
140
db/schema/tenants.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
integer,
|
||||
smallint,
|
||||
date,
|
||||
uuid,
|
||||
pgEnum,
|
||||
} from "drizzle-orm/pg-core"
|
||||
import { authUsers } from "./auth_users"
|
||||
import {lockedTenantEnum} from "./enums";
|
||||
|
||||
export const tenants = pgTable(
|
||||
"tenants",
|
||||
{
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
short: text("short").notNull(),
|
||||
|
||||
calendarConfig: jsonb("calendarConfig").default({
|
||||
eventTypes: [
|
||||
{ color: "blue", label: "Büro" },
|
||||
{ color: "yellow", label: "Besprechung" },
|
||||
{ color: "green", label: "Umsetzung" },
|
||||
{ color: "red", label: "Vor Ort Termin" },
|
||||
],
|
||||
}),
|
||||
|
||||
timeConfig: jsonb("timeConfig").notNull().default({}),
|
||||
|
||||
tags: jsonb("tags").notNull().default({
|
||||
products: [],
|
||||
documents: [],
|
||||
}),
|
||||
|
||||
measures: jsonb("measures")
|
||||
.notNull()
|
||||
.default([
|
||||
{ name: "Netzwerktechnik", short: "NWT" },
|
||||
{ name: "Elektrotechnik", short: "ELT" },
|
||||
{ name: "Photovoltaik", short: "PV" },
|
||||
{ name: "Videüberwachung", short: "VÜA" },
|
||||
{ name: "Projekt", short: "PRJ" },
|
||||
{ name: "Smart Home", short: "SHO" },
|
||||
]),
|
||||
|
||||
businessInfo: jsonb("businessInfo").default({
|
||||
zip: "",
|
||||
city: "",
|
||||
name: "",
|
||||
street: "",
|
||||
}),
|
||||
|
||||
features: jsonb("features").default({
|
||||
objects: true,
|
||||
calendar: true,
|
||||
contacts: true,
|
||||
projects: true,
|
||||
vehicles: true,
|
||||
contracts: true,
|
||||
inventory: true,
|
||||
accounting: true,
|
||||
timeTracking: true,
|
||||
planningBoard: true,
|
||||
workingTimeTracking: true,
|
||||
}),
|
||||
|
||||
ownFields: jsonb("ownFields"),
|
||||
|
||||
numberRanges: jsonb("numberRanges")
|
||||
.notNull()
|
||||
.default({
|
||||
vendors: { prefix: "", suffix: "", nextNumber: 10000 },
|
||||
customers: { prefix: "", suffix: "", nextNumber: 10000 },
|
||||
products: { prefix: "AT-", suffix: "", nextNumber: 1000 },
|
||||
quotes: { prefix: "AN-", suffix: "", nextNumber: 1000 },
|
||||
confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 },
|
||||
invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 },
|
||||
spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 },
|
||||
inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 },
|
||||
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
|
||||
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
|
||||
}),
|
||||
|
||||
standardEmailForInvoices: text("standardEmailForInvoices"),
|
||||
|
||||
extraModules: jsonb("extraModules").notNull().default([]),
|
||||
|
||||
isInTrial: boolean("isInTrial").default(false),
|
||||
trialEndDate: date("trialEndDate"),
|
||||
|
||||
stripeCustomerId: text("stripeCustomerId"),
|
||||
|
||||
hasActiveLicense: boolean("hasActiveLicense").notNull().default(false),
|
||||
|
||||
userLicenseCount: integer("userLicenseCount")
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
workstationLicenseCount: integer("workstationLicenseCount")
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
standardPaymentDays: smallint("standardPaymentDays")
|
||||
.notNull()
|
||||
.default(14),
|
||||
|
||||
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
|
||||
|
||||
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),
|
||||
|
||||
autoPrepareIncomingInvoices: boolean("autoPrepareIncomingInvoices")
|
||||
.default(true),
|
||||
|
||||
portalDomain: text("portalDomain"),
|
||||
|
||||
portalConfig: jsonb("portalConfig")
|
||||
.notNull()
|
||||
.default({ primayColor: "#69c350" }),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
locked: lockedTenantEnum("locked"),
|
||||
}
|
||||
)
|
||||
|
||||
export type Tenant = typeof tenants.$inferSelect
|
||||
export type NewTenant = typeof tenants.$inferInsert
|
||||
44
db/schema/texttemplates.ts
Normal file
44
db/schema/texttemplates.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { textTemplatePositionsEnum } from "./enums"
|
||||
|
||||
export const texttemplates = pgTable("texttemplates", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
text: text("text").notNull(),
|
||||
|
||||
documentType: text("documentType").default(""),
|
||||
|
||||
default: boolean("default").notNull().default(false),
|
||||
|
||||
pos: textTemplatePositionsEnum("pos").notNull(),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type TextTemplate = typeof texttemplates.$inferSelect
|
||||
export type NewTextTemplate = typeof texttemplates.$inferInsert
|
||||
27
db/schema/units.ts
Normal file
27
db/schema/units.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
export const units = pgTable("units", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
single: text("single").notNull(),
|
||||
|
||||
multiple: text("multiple"),
|
||||
short: text("short"),
|
||||
|
||||
step: text("step").notNull().default("1"),
|
||||
})
|
||||
|
||||
export type Unit = typeof units.$inferSelect
|
||||
export type NewUnit = typeof units.$inferInsert
|
||||
53
db/schema/user_credentials.ts
Normal file
53
db/schema/user_credentials.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
timestamp,
|
||||
bigint,
|
||||
boolean,
|
||||
jsonb,
|
||||
numeric, pgEnum,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import {credentialTypesEnum} from "./enums";
|
||||
|
||||
|
||||
|
||||
export const userCredentials = pgTable("user_credentials", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
tenantId: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
smtpPort: numeric("smtp_port"),
|
||||
smtpSsl: boolean("smtp_ssl"),
|
||||
|
||||
type: credentialTypesEnum("type").notNull(),
|
||||
|
||||
imapPort: numeric("imap_port"),
|
||||
imapSsl: boolean("imap_ssl"),
|
||||
|
||||
emailEncrypted: jsonb("email_encrypted"),
|
||||
passwordEncrypted: jsonb("password_encrypted"),
|
||||
|
||||
smtpHostEncrypted: jsonb("smtp_host_encrypted"),
|
||||
imapHostEncrypted: jsonb("imap_host_encrypted"),
|
||||
|
||||
accessTokenEncrypted: jsonb("access_token_encrypted"),
|
||||
refreshTokenEncrypted: jsonb("refresh_token_encrypted"),
|
||||
})
|
||||
|
||||
export type UserCredential = typeof userCredentials.$inferSelect
|
||||
export type NewUserCredential = typeof userCredentials.$inferInsert
|
||||
57
db/schema/vehicles.ts
Normal file
57
db/schema/vehicles.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
doublePrecision,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const vehicles = pgTable("vehicles", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
license_plate: text("licensePlate"),
|
||||
name: text("name"),
|
||||
type: text("type"),
|
||||
|
||||
active: boolean("active").default(true),
|
||||
|
||||
// FIXED: driver references auth_users.id
|
||||
driver: uuid("driver").references(() => authUsers.id),
|
||||
|
||||
vin: text("vin"),
|
||||
|
||||
tank_size: doublePrecision("tankSize").notNull().default(0),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
build_year: text("buildYear"),
|
||||
|
||||
towing_capacity: bigint("towingCapacity", { mode: "number" }),
|
||||
power_in_kw: bigint("powerInKW", { mode: "number" }),
|
||||
|
||||
color: text("color"),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Vehicle = typeof vehicles.$inferSelect
|
||||
export type NewVehicle = typeof vehicles.$inferInsert
|
||||
45
db/schema/vendors.ts
Normal file
45
db/schema/vendors.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const vendors = pgTable("vendors", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
vendorNumber: text("vendorNumber").notNull(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
infoData: jsonb("infoData").notNull().default({}),
|
||||
notes: text("notes"),
|
||||
|
||||
hasSEPA: boolean("hasSEPA").notNull().default(false),
|
||||
|
||||
profiles: jsonb("profiles").notNull().default([]),
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
defaultPaymentMethod: text("defaultPaymentMethod"),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type Vendor = typeof vendors.$inferSelect
|
||||
export type NewVendor = typeof vendors.$inferInsert
|
||||
11
drizzle.config.ts
Normal file
11
drizzle.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "drizzle-kit"
|
||||
import {secrets} from "./src/utils/secrets";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: "./db/schema",
|
||||
out: "./db/migrations",
|
||||
dbCredentials: {
|
||||
url: secrets.DATABASE_URL || process.env.DATABASE_URL,
|
||||
},
|
||||
})
|
||||
9635
package-lock.json
generated
9635
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
"start": "node dist/index.js",
|
||||
"schema:index": "ts-node scripts/generate-schema-index.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,26 +25,35 @@
|
||||
"@fastify/swagger": "^9.5.1",
|
||||
"@fastify/swagger-ui": "^5.2.3",
|
||||
"@infisical/sdk": "^4.0.6",
|
||||
"@mmote/niimbluelib": "^0.0.1-alpha.29",
|
||||
"@prisma/client": "^6.15.0",
|
||||
"@supabase/supabase-js": "^2.56.1",
|
||||
"@zip.js/zip.js": "^2.7.73",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.1",
|
||||
"bcrypt": "^6.0.0",
|
||||
"bwip-js": "^4.8.0",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"drizzle-orm": "^0.45.0",
|
||||
"fastify": "^5.5.0",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"imapflow": "^1.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"nodemailer": "^7.0.6",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"xmlbuilder": "^15.1.1"
|
||||
"pg": "^8.16.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"zpl-image": "^0.2.0",
|
||||
"zpl-renderer-js": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"prisma": "^6.15.0",
|
||||
"tsx": "^4.20.5",
|
||||
"typescript": "^5.9.2"
|
||||
|
||||
16
scripts/generate-schema-index.ts
Normal file
16
scripts/generate-schema-index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const schemaDir = path.resolve("db/schema")
|
||||
const indexFile = path.join(schemaDir, "index.ts")
|
||||
|
||||
const files = fs
|
||||
.readdirSync(schemaDir)
|
||||
.filter((f) => f.endsWith(".ts") && f !== "index.ts")
|
||||
|
||||
const exportsToWrite = files
|
||||
.map((f) => `export * from "./${f.replace(".ts", "")}"`)
|
||||
.join("\n")
|
||||
|
||||
fs.writeFileSync(indexFile, exportsToWrite)
|
||||
console.log("✓ schema/index.ts generated")
|
||||
20
src/index.ts
20
src/index.ts
@@ -12,12 +12,11 @@ import authPlugin from "./plugins/auth";
|
||||
import adminRoutes from "./routes/admin";
|
||||
import corsPlugin from "./plugins/cors";
|
||||
import queryConfigPlugin from "./plugins/queryconfig";
|
||||
import resourceRoutes from "./routes/resources";
|
||||
import dbPlugin from "./plugins/db";
|
||||
import resourceRoutesSpecial from "./routes/resourcesSpecial";
|
||||
import fastifyCookie from "@fastify/cookie";
|
||||
import historyRoutes from "./routes/history";
|
||||
import fileRoutes from "./routes/files";
|
||||
import userRoutes from "./routes/auth/user"
|
||||
import functionRoutes from "./routes/functions";
|
||||
import bankingRoutes from "./routes/banking";
|
||||
import exportRoutes from "./routes/exports"
|
||||
@@ -29,6 +28,9 @@ import notificationsRoutes from "./routes/notifications";
|
||||
import staffTimeRoutes from "./routes/staff/time";
|
||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||
|
||||
//Resources
|
||||
import resourceRoutes from "./routes/resources/main";
|
||||
|
||||
//M2M
|
||||
import authM2m from "./plugins/auth.m2m";
|
||||
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
|
||||
@@ -57,6 +59,7 @@ async function main() {
|
||||
await app.register(supabasePlugin);
|
||||
await app.register(tenantPlugin);
|
||||
await app.register(dayjsPlugin);
|
||||
await app.register(dbPlugin);
|
||||
|
||||
app.addHook('preHandler', (req, reply, done) => {
|
||||
console.log(req.method)
|
||||
@@ -97,11 +100,9 @@ async function main() {
|
||||
await subApp.register(meRoutes);
|
||||
await subApp.register(tenantRoutes);
|
||||
await subApp.register(adminRoutes);
|
||||
await subApp.register(resourceRoutes);
|
||||
await subApp.register(resourceRoutesSpecial);
|
||||
await subApp.register(historyRoutes);
|
||||
await subApp.register(fileRoutes);
|
||||
await subApp.register(userRoutes);
|
||||
await subApp.register(functionRoutes);
|
||||
await subApp.register(bankingRoutes);
|
||||
await subApp.register(exportRoutes);
|
||||
@@ -112,8 +113,19 @@ async function main() {
|
||||
await subApp.register(staffTimeRoutes);
|
||||
await subApp.register(staffTimeConnectRoutes);
|
||||
|
||||
|
||||
await subApp.register(resourceRoutes);
|
||||
|
||||
},{prefix: "/api"})
|
||||
|
||||
app.ready(async () => {
|
||||
try {
|
||||
const result = await app.db.execute("SELECT NOW()");
|
||||
console.log("✓ DB connection OK: " + JSON.stringify(result.rows[0]));
|
||||
} catch (err) {
|
||||
console.log("❌ DB connection failed:", err);
|
||||
}
|
||||
});
|
||||
|
||||
// Start
|
||||
try {
|
||||
|
||||
@@ -72,8 +72,8 @@ export async function generateTimesEvaluation(
|
||||
|
||||
for (const t of times) {
|
||||
const minutes = calcMinutes(t.started_at, t.stopped_at)
|
||||
if(["submitted","approved"].includes(t.state))sumWorkingMinutesEingereicht += minutes
|
||||
if (t.state === "approved") sumWorkingMinutesApproved += minutes
|
||||
if(["submitted","approved"].includes(t.state) && t.type === "work")sumWorkingMinutesEingereicht += minutes
|
||||
if (t.state === "approved" && t.type === "work") sumWorkingMinutesApproved += minutes
|
||||
}
|
||||
|
||||
// 🎉 Feiertagsausgleich
|
||||
@@ -90,16 +90,37 @@ export async function generateTimesEvaluation(
|
||||
}
|
||||
|
||||
// 🏖️ Urlaub & Krankheit (über Typ)
|
||||
const sumWorkingMinutesVacationDays = times
|
||||
.filter((t) => t.type === "vacation")
|
||||
.reduce((sum, t) => sum + calcMinutes(t.started_at, t.stopped_at), 0)
|
||||
let sumWorkingMinutesVacationDays = 0
|
||||
let sumVacationDays = 0
|
||||
times
|
||||
.filter((t) => t.type === "vacation" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
const days = server.dayjs(time.stopped_at).diff(server.dayjs(time.startet_at), "day") + 1;
|
||||
|
||||
const sumWorkingMinutesSickDays = times
|
||||
.filter((t) => t.type === "sick")
|
||||
.reduce((sum, t) => sum + calcMinutes(t.started_at, t.stopped_at), 0)
|
||||
for(let i = 0; i < days; i++) {
|
||||
const weekday = server.dayjs(time.started_at).add(i,"day").day()
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||
sumWorkingMinutesVacationDays += hours * 60
|
||||
}
|
||||
sumVacationDays += days
|
||||
})
|
||||
|
||||
const sumVacationDays = times.filter((t) => t.type === "vacation").length
|
||||
const sumSickDays = times.filter((t) => t.type === "sick").length
|
||||
let sumWorkingMinutesSickDays = 0
|
||||
let sumSickDays = 0
|
||||
|
||||
times
|
||||
.filter((t) => t.type === "sick" && t.state === "approved")
|
||||
.forEach((time) => {
|
||||
const days = server.dayjs(time.stopped_at).diff(server.dayjs(time.startet_at), "day") + 1;
|
||||
|
||||
for(let i = 0; i < days; i++) {
|
||||
const weekday = server.dayjs(time.started_at).add(i,"day").day()
|
||||
const hours = profile.weekly_regular_working_hours?.[weekday] || 0
|
||||
sumWorkingMinutesSickDays += hours * 60
|
||||
}
|
||||
|
||||
sumSickDays += days
|
||||
})
|
||||
|
||||
// 💰 Salden
|
||||
const saldo =
|
||||
|
||||
@@ -6,7 +6,10 @@ export default fp(async (server: FastifyInstance) => {
|
||||
await server.register(cors, {
|
||||
origin: [
|
||||
"http://localhost:3000", // dein Nuxt-Frontend
|
||||
"http://localhost:3001", // dein Nuxt-Frontend
|
||||
"http://127.0.0.1:3000", // dein Nuxt-Frontend
|
||||
"http://192.168.1.227:3001", // dein Nuxt-Frontend
|
||||
"http://192.168.1.227:3000", // dein Nuxt-Frontend
|
||||
"https://beta.fedeo.de", // dein Nuxt-Frontend
|
||||
"https://app.fedeo.de", // dein Nuxt-Frontend
|
||||
"capacitor://localhost", // dein Nuxt-Frontend
|
||||
|
||||
34
src/plugins/db.ts
Normal file
34
src/plugins/db.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import fp from "fastify-plugin"
|
||||
import {drizzle, NodePgDatabase} from "drizzle-orm/node-postgres"
|
||||
import { Pool } from "pg"
|
||||
import * as schema from "../../db/schema"
|
||||
|
||||
export default fp(async (server, opts) => {
|
||||
const pool = new Pool({
|
||||
host: "db-001.netbird.cloud",
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
user: "postgres",
|
||||
password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu",
|
||||
database: "fedeo",
|
||||
ssl: process.env.DB_DISABLE_SSL === "true" ? false : undefined,
|
||||
})
|
||||
|
||||
// Drizzle instance
|
||||
const db = drizzle(pool, { schema })
|
||||
|
||||
// Dekorieren -> überall server.db
|
||||
server.decorate("db", db)
|
||||
|
||||
// Graceful Shutdown
|
||||
server.addHook("onClose", async () => {
|
||||
await pool.end()
|
||||
})
|
||||
|
||||
server.log.info("Drizzle database connected")
|
||||
})
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyInstance {
|
||||
db:NodePgDatabase<typeof schema>
|
||||
}
|
||||
}
|
||||
114
src/resource.config.ts
Normal file
114
src/resource.config.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
contacts,
|
||||
contracts, costcentres, createddocuments,
|
||||
customers,
|
||||
files, filetags, folders, hourrates, inventoryitemgroups,
|
||||
inventoryitems, letterheads, ownaccounts,
|
||||
plants, productcategories, products,
|
||||
projects,
|
||||
projecttypes, servicecategories, services, spaces, tasks, texttemplates, units, vehicles,
|
||||
vendors
|
||||
} from "../db/schema";
|
||||
|
||||
export const resourceConfig = {
|
||||
projects: {
|
||||
searchColumns: ["name"],
|
||||
mtoLoad: ["customer","plant","contract","projecttype"],
|
||||
mtmLoad: ["tasks", "files"],
|
||||
table: projects
|
||||
},
|
||||
customers: {
|
||||
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
|
||||
mtmLoad: ["contacts","projects"],
|
||||
table: customers,
|
||||
},
|
||||
contacts: {
|
||||
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
|
||||
table: contacts,
|
||||
mtoLoad: ["customer","vendor"]
|
||||
},
|
||||
contracts: {
|
||||
table: contracts,
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"]
|
||||
},
|
||||
plants: {
|
||||
table: plants,
|
||||
mtoLoad: ["customer"],
|
||||
mtmLoad: ["projects","tasks","files"],
|
||||
},
|
||||
projecttypes: {
|
||||
table: projecttypes
|
||||
},
|
||||
vendors: {
|
||||
table: vendors,
|
||||
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
|
||||
},
|
||||
files: {
|
||||
table: files
|
||||
},
|
||||
folders: {
|
||||
table: folders
|
||||
},
|
||||
filetags: {
|
||||
table: filetags
|
||||
},
|
||||
inventoryitems: {
|
||||
table: inventoryitems
|
||||
},
|
||||
inventoryitemgroups: {
|
||||
table: inventoryitemgroups
|
||||
},
|
||||
products: {
|
||||
table: products,
|
||||
searchColumns: ["name","manufacturer","ean","barcode","description","manfacturer_number","article_number"],
|
||||
},
|
||||
productcategories: {
|
||||
table: productcategories
|
||||
},
|
||||
services: {
|
||||
table: services,
|
||||
mtoLoad: ["unit"],
|
||||
searchColumns: ["name","description"],
|
||||
},
|
||||
servicecategories: {
|
||||
table: servicecategories
|
||||
},
|
||||
units: {
|
||||
table: units,
|
||||
},
|
||||
vehicles: {
|
||||
table: vehicles,
|
||||
searchColumns: ["name","license_plate","vin","color"],
|
||||
},
|
||||
hourrates: {
|
||||
table: hourrates,
|
||||
searchColumns: ["name"],
|
||||
},
|
||||
spaces: {
|
||||
table: spaces,
|
||||
searchColumns: ["name","space_number","type","info_data"],
|
||||
},
|
||||
ownaccounts: {
|
||||
table: ownaccounts,
|
||||
searchColumns: ["name","description","number"],
|
||||
},
|
||||
costcentres: {
|
||||
table: costcentres,
|
||||
searchColumns: ["name","number","description"],
|
||||
mtoLoad: ["vehicle","project","inventoryitem"]
|
||||
},
|
||||
tasks: {
|
||||
table: tasks,
|
||||
},
|
||||
letterheads: {
|
||||
table: letterheads,
|
||||
|
||||
},
|
||||
createddocuments: {
|
||||
table: createddocuments,
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead",]
|
||||
},
|
||||
texttemplates: {
|
||||
table: texttemplates
|
||||
}
|
||||
}
|
||||
@@ -1,94 +1,117 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import {
|
||||
authTenantUsers,
|
||||
authUsers,
|
||||
tenants,
|
||||
} from "../../db/schema";
|
||||
|
||||
export default async function adminRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POST /admin/add-user-to-tenant
|
||||
// -------------------------------------------------------------
|
||||
server.post("/admin/add-user-to-tenant", async (req, reply) => {
|
||||
const body = req.body as {
|
||||
user_id: string;
|
||||
tenant_id: string;
|
||||
role?: string;
|
||||
mode?: "single" | "multi";
|
||||
};
|
||||
try {
|
||||
const body = req.body as {
|
||||
user_id: string;
|
||||
tenant_id: number;
|
||||
role?: string;
|
||||
mode?: "single" | "multi";
|
||||
};
|
||||
|
||||
if (!body.user_id || !body.tenant_id) {
|
||||
return reply.code(400).send({ error: "user_id and tenant_id required" });
|
||||
if (!body.user_id || !body.tenant_id) {
|
||||
return reply.code(400).send({
|
||||
error: "user_id and tenant_id required"
|
||||
});
|
||||
}
|
||||
|
||||
const mode = body.mode ?? "multi";
|
||||
|
||||
// ----------------------------
|
||||
// SINGLE MODE → alte Verknüpfungen löschen
|
||||
// ----------------------------
|
||||
if (mode === "single") {
|
||||
await server.db
|
||||
.delete(authTenantUsers)
|
||||
.where(eq(authTenantUsers.user_id, body.user_id));
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// Neue Verknüpfung hinzufügen
|
||||
// ----------------------------
|
||||
|
||||
await server.db
|
||||
.insert(authTenantUsers)
|
||||
// @ts-ignore
|
||||
.values({
|
||||
user_id: body.user_id,
|
||||
tenantId: body.tenant_id,
|
||||
role: body.role ?? "member",
|
||||
});
|
||||
|
||||
return { success: true, mode };
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR /admin/add-user-to-tenant:", err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
|
||||
// Default: "multi"
|
||||
const mode = body.mode ?? "multi";
|
||||
|
||||
if (mode === "single") {
|
||||
// Erst alle alten Verknüpfungen löschen
|
||||
await server.supabase
|
||||
.from("auth_tenant_users")
|
||||
.delete()
|
||||
.eq("user_id", body.user_id);
|
||||
}
|
||||
|
||||
const { error } = await server.supabase
|
||||
.from("auth_tenant_users")
|
||||
.insert({
|
||||
tenant_id: body.tenant_id,
|
||||
user_id: body.user_id,
|
||||
role: body.role ?? "member",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
// Neuen Eintrag setzen
|
||||
|
||||
|
||||
return { success: true, mode };
|
||||
});
|
||||
|
||||
/**
|
||||
* Alle Tenants eines Users abfragen
|
||||
*/
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /admin/user-tenants/:user_id
|
||||
// -------------------------------------------------------------
|
||||
server.get("/admin/user-tenants/:user_id", async (req, reply) => {
|
||||
const { user_id } = req.params as { user_id: string };
|
||||
try {
|
||||
const { user_id } = req.params as { user_id: string };
|
||||
|
||||
if (!user_id) {
|
||||
return reply.code(400).send({ error: "user_id required" });
|
||||
if (!user_id) {
|
||||
return reply.code(400).send({ error: "user_id required" });
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// 1) User existiert?
|
||||
// ----------------------------
|
||||
const [user] = await server.db
|
||||
.select()
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, user_id))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
return reply.code(400).send({ error: "faulty user_id presented" });
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// 2) Tenants Join über auth_tenant_users
|
||||
// ----------------------------
|
||||
const tenantRecords = await server.db
|
||||
.select({
|
||||
id: tenants.id,
|
||||
name: tenants.name,
|
||||
short: tenants.short,
|
||||
locked: tenants.locked,
|
||||
numberRanges: tenants.numberRanges,
|
||||
extraModules: tenants.extraModules,
|
||||
})
|
||||
.from(authTenantUsers)
|
||||
.innerJoin(
|
||||
tenants,
|
||||
eq(authTenantUsers.tenant_id, tenants.id)
|
||||
)
|
||||
.where(eq(authTenantUsers.user_id, user_id));
|
||||
|
||||
return {
|
||||
user_id,
|
||||
tenants: tenantRecords,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR /admin/user-tenants:", err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
|
||||
const {data:user, error: userError} = await server.supabase.from("auth_users").select("*,tenants(*)").eq("id", user_id).single();
|
||||
|
||||
console.log(userError)
|
||||
console.log(user)
|
||||
|
||||
if(!user) {
|
||||
return reply.code(400).send({ error: "faulty user_id presented" });
|
||||
} else {
|
||||
return { user_id, tenants: user.tenants };
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Alle User eines Tenants abfragen
|
||||
* TODO: Aktuell nur Multi Tenant
|
||||
*/
|
||||
/*server.get("/admin/tenant-users/:tenant_id", async (req, reply) => {
|
||||
const { tenant_id } = req.params as { tenant_id: string };
|
||||
|
||||
if (!tenant_id) {
|
||||
return reply.code(400).send({ error: "tenant_id required" });
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_tenant_users")
|
||||
.select(`
|
||||
user_id,
|
||||
role,
|
||||
users ( id, email, created_at )
|
||||
`)
|
||||
.eq("tenant_id", tenant_id);
|
||||
|
||||
if (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
return { tenant_id, users: data };
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import bcrypt from "bcrypt";
|
||||
import { FastifyInstance } from "fastify"
|
||||
import bcrypt from "bcrypt"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { authUsers } from "../../../db/schema" // wichtig: Drizzle Schema importieren!
|
||||
|
||||
export default async function authRoutesAuthenticated(server: FastifyInstance) {
|
||||
|
||||
server.post("/auth/password/change", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
summary: "Reset Password after forced change",
|
||||
summary: "Change password (after login or forced reset)",
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["old_password", "new_password"],
|
||||
@@ -25,54 +28,69 @@ export default async function authRoutesAuthenticated(server: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const { old_password, new_password } = req.body as { old_password: string; new_password: string };
|
||||
|
||||
console.log(req.user)
|
||||
try {
|
||||
const { old_password, new_password } = req.body as {
|
||||
old_password: string
|
||||
new_password: string
|
||||
}
|
||||
|
||||
const user_id = req.user?.user_id; // kommt aus JWT Middleware
|
||||
if (!user_id) {
|
||||
// @ts-ignore
|
||||
return reply.code(401).send({ error: "Unauthorized" });
|
||||
const userId = req.user?.user_id
|
||||
if (!userId) {
|
||||
//@ts-ignore
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 1) User laden
|
||||
// -----------------------------------------------------
|
||||
const [user] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
passwordHash: authUsers.passwordHash,
|
||||
mustChangePassword: authUsers.must_change_password
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.id, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!user) {
|
||||
//@ts-ignore
|
||||
return reply.code(404).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 2) Altes PW prüfen
|
||||
// -----------------------------------------------------
|
||||
const valid = await bcrypt.compare(old_password, user.passwordHash)
|
||||
if (!valid) {
|
||||
//@ts-ignore
|
||||
return reply.code(401).send({ error: "Old password incorrect" })
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 3) Neues PW hashen
|
||||
// -----------------------------------------------------
|
||||
const newHash = await bcrypt.hash(new_password, 10)
|
||||
|
||||
// -----------------------------------------------------
|
||||
// 4) Updaten
|
||||
// -----------------------------------------------------
|
||||
await server.db
|
||||
.update(authUsers)
|
||||
.set({
|
||||
passwordHash: newHash,
|
||||
must_change_password: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(authUsers.id, userId))
|
||||
|
||||
return { success: true }
|
||||
|
||||
} catch (err) {
|
||||
console.error("POST /auth/password/change ERROR:", err)
|
||||
//@ts-ignore
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
|
||||
// Nutzer laden
|
||||
const { data: user, error } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("id, password_hash, must_change_password")
|
||||
.eq("id", user_id)
|
||||
.single();
|
||||
|
||||
if (error || !user) {
|
||||
// @ts-ignore
|
||||
return reply.code(404).send({ error: "User not found" });
|
||||
}
|
||||
|
||||
// Altes Passwort prüfen
|
||||
const valid = await bcrypt.compare(old_password, user.password_hash);
|
||||
if (!valid) {
|
||||
// @ts-ignore
|
||||
return reply.code(401).send({ error: "Old password incorrect" });
|
||||
}
|
||||
|
||||
// Neues Passwort hashen
|
||||
const newHash = await bcrypt.hash(new_password, 10);
|
||||
|
||||
// Speichern + Flag zurücksetzen
|
||||
const { error: updateError } = await server.supabase
|
||||
.from("auth_users")
|
||||
.update({
|
||||
password_hash: newHash,
|
||||
must_change_password: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", user_id);
|
||||
|
||||
if (updateError) {
|
||||
console.log(updateError);
|
||||
// @ts-ignore
|
||||
return reply.code(500).send({ error: "Password update failed" });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import bcrypt from "bcrypt";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { generateRandomPassword, hashPassword } from "../../utils/password"
|
||||
import { sendMail } from "../../utils/mailer"
|
||||
import {secrets} from "../../utils/secrets";
|
||||
import { generateRandomPassword, hashPassword } from "../../utils/password";
|
||||
import { sendMail } from "../../utils/mailer";
|
||||
import { secrets } from "../../utils/secrets";
|
||||
|
||||
import { authUsers } from "../../../db/schema";
|
||||
import { authTenantUsers } from "../../../db/schema";
|
||||
import { tenants } from "../../../db/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
export default async function authRoutes(server: FastifyInstance) {
|
||||
// Registrierung
|
||||
server.post("/auth/register",{
|
||||
|
||||
// -----------------------------------------------------
|
||||
// REGISTER
|
||||
// -----------------------------------------------------
|
||||
server.post("/auth/register", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
summary: "Register User",
|
||||
@@ -19,43 +27,31 @@ export default async function authRoutes(server: FastifyInstance) {
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: "object",
|
||||
properties: {
|
||||
user: { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const body = req.body as { email: string; password: string };
|
||||
|
||||
if (!body.email || !body.password) {
|
||||
// @ts-ignore
|
||||
return reply.code(400).send({ error: "Email and password required" });
|
||||
}
|
||||
|
||||
// Passwort hashen
|
||||
const passwordHash = await bcrypt.hash(body.password, 10);
|
||||
|
||||
// User speichern
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_users")
|
||||
.insert({ email: body.email, password_hash: passwordHash })
|
||||
.select("id, email")
|
||||
.single();
|
||||
const [user] = await server.db
|
||||
.insert(authUsers)
|
||||
.values({
|
||||
email: body.email.toLowerCase(),
|
||||
passwordHash,
|
||||
})
|
||||
.returning({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
// @ts-ignore
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
return { user: data };
|
||||
return { user };
|
||||
});
|
||||
|
||||
// Login
|
||||
server.post("/auth/login",{
|
||||
|
||||
// -----------------------------------------------------
|
||||
// LOGIN
|
||||
// -----------------------------------------------------
|
||||
server.post("/auth/login", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
summary: "Login User",
|
||||
@@ -67,103 +63,110 @@ export default async function authRoutes(server: FastifyInstance) {
|
||||
password: { type: "string" },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: "object",
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, async (req, reply) => {
|
||||
const body = req.body as { email: string; password: string };
|
||||
|
||||
if (!body.email || !body.password) {
|
||||
// @ts-ignore
|
||||
return reply.code(400).send({ error: "Email and password required" });
|
||||
}
|
||||
let user: any = null;
|
||||
|
||||
/**
|
||||
* Wenn das Tenant Objekt verfügbar ist, befindet sich das Backend im Single Tenant Modus.
|
||||
* Es werden nur Benutzer zugelassen, welche auschließlich diesem Tenant angehören.
|
||||
* Das zeigt sich über das im User gesetzte Tenant Feld
|
||||
*
|
||||
* */
|
||||
let user = null
|
||||
let error = null
|
||||
if(req.tenant) {
|
||||
// User finden
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("*, tenants!auth_tenant_users(*)")
|
||||
.eq("email", body.email)
|
||||
// -------------------------------
|
||||
// SINGLE TENANT MODE
|
||||
// -------------------------------
|
||||
/* if (req.tenant) {
|
||||
const tenantId = req.tenant.id;
|
||||
|
||||
// @ts-ignore
|
||||
user = (data || []).find(i => i.tenants.find(x => x.id === req.tenant.id))
|
||||
if(error) {
|
||||
// @ts-ignore
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
} else {
|
||||
// User finden
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("*")
|
||||
.eq("email", body.email)
|
||||
.single();
|
||||
user = data
|
||||
if(error) {
|
||||
// @ts-ignore
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
}
|
||||
|
||||
if(!user) {
|
||||
// @ts-ignore
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
} else {
|
||||
|
||||
const valid = await bcrypt.compare(body.password, user.password_hash);
|
||||
if (!valid) {
|
||||
// @ts-ignore
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
} else {
|
||||
const token = jwt.sign(
|
||||
{ user_id: user.id, email: user.email, tenant_id: req.tenant?.id ? req.tenant.id : null },
|
||||
secrets.JWT_SECRET!,
|
||||
{ expiresIn: "6h" }
|
||||
);
|
||||
|
||||
reply.setCookie("token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
secure: process.env.NODE_ENV === "production", // lokal: false, prod: true
|
||||
maxAge: 60 * 60 * 3, // 3 Stunden
|
||||
const result = await server.db
|
||||
.select({
|
||||
user: authUsers,
|
||||
})
|
||||
.from(authUsers)
|
||||
.innerJoin(
|
||||
authTenantUsers,
|
||||
eq(authTenantUsers.userId, authUsers.id)
|
||||
)
|
||||
.innerJoin(
|
||||
tenants,
|
||||
eq(authTenantUsers.tenantId, tenants.id)
|
||||
)
|
||||
.where(and(
|
||||
eq(authUsers.email, body.email.toLowerCase()),
|
||||
eq(authTenantUsers.tenantId, tenantId)
|
||||
));
|
||||
|
||||
return { token };
|
||||
if (result.length === 0) {
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
user = result[0].user;
|
||||
|
||||
// -------------------------------
|
||||
// MULTI TENANT MODE
|
||||
// -------------------------------
|
||||
} else {*/
|
||||
const [found] = await server.db
|
||||
.select()
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.email, body.email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (!found) {
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
user = found;
|
||||
/*}*/
|
||||
|
||||
// Passwort prüfen
|
||||
const valid = await bcrypt.compare(body.password, user.passwordHash);
|
||||
if (!valid) {
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
user_id: user.id,
|
||||
email: user.email,
|
||||
tenant_id: req.tenant?.id ?? null,
|
||||
},
|
||||
secrets.JWT_SECRET!,
|
||||
{ expiresIn: "6h" }
|
||||
);
|
||||
|
||||
reply.setCookie("token", token, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
maxAge: 60 * 60 * 3,
|
||||
});
|
||||
|
||||
return { token };
|
||||
});
|
||||
|
||||
|
||||
// -----------------------------------------------------
|
||||
// LOGOUT
|
||||
// -----------------------------------------------------
|
||||
server.post("/auth/logout", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
summary: "Logout User (löscht Cookie)"
|
||||
},
|
||||
summary: "Logout User"
|
||||
}
|
||||
}, async (req, reply) => {
|
||||
reply.clearCookie("token", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
})
|
||||
});
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
|
||||
// -----------------------------------------------------
|
||||
// PASSWORD RESET
|
||||
// -----------------------------------------------------
|
||||
server.post("/auth/password/reset", {
|
||||
schema: {
|
||||
tags: ["Auth"],
|
||||
@@ -177,43 +180,45 @@ export default async function authRoutes(server: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
}, async (req, reply) => {
|
||||
const { email } = req.body as { email: string }
|
||||
const { email } = req.body as { email: string };
|
||||
|
||||
// User finden
|
||||
const { data: user, error } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("id, email")
|
||||
.eq("email", email)
|
||||
.single()
|
||||
const [user] = await server.db
|
||||
.select({
|
||||
id: authUsers.id,
|
||||
email: authUsers.email,
|
||||
})
|
||||
.from(authUsers)
|
||||
.where(eq(authUsers.email, email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (error || !user) {
|
||||
return reply.code(404).send({ error: "User not found" })
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: "User not found" });
|
||||
}
|
||||
|
||||
// Neues Passwort generieren
|
||||
const plainPassword = generateRandomPassword()
|
||||
const passwordHash = await hashPassword(plainPassword)
|
||||
const plainPassword = generateRandomPassword();
|
||||
const passwordHash = await hashPassword(plainPassword);
|
||||
|
||||
// In DB updaten
|
||||
const { error: updateError } = await server.supabase
|
||||
.from("auth_users")
|
||||
.update({ password_hash: passwordHash, must_change_password: true })
|
||||
.eq("id", user.id)
|
||||
|
||||
if (updateError) {
|
||||
return reply.code(500).send({ error: "Could not update password" })
|
||||
}
|
||||
await server.db
|
||||
.update(authUsers)
|
||||
.set({
|
||||
passwordHash,
|
||||
// @ts-ignore
|
||||
mustChangePassword: true,
|
||||
})
|
||||
.where(eq(authUsers.id, user.id));
|
||||
|
||||
// Mail verschicken
|
||||
await sendMail(
|
||||
user.email,
|
||||
"FEDEO | Dein neues Passwort",
|
||||
`<p>Hallo,</p>
|
||||
<p>dein Passwort wurde zurückgesetzt.</p>
|
||||
<p><strong>Neues Passwort:</strong> ${plainPassword}</p>
|
||||
<p>Bitte ändere es nach dem Login umgehend.</p>`
|
||||
)
|
||||
`
|
||||
<p>Hallo,</p>
|
||||
<p>Dein Passwort wurde zurückgesetzt.</p>
|
||||
<p><strong>Neues Passwort:</strong> ${plainPassword}</p>
|
||||
<p>Bitte ändere es nach dem Login umgehend.</p>
|
||||
`
|
||||
);
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
server.get("/me", async (req, reply) => {
|
||||
const authUser = req.user // kommt aus JWT (user_id + tenant_id)
|
||||
try {
|
||||
const authUser = req.user
|
||||
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
if (!authUser) {
|
||||
return reply.code(401).send({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const user_id = req.user.user_id
|
||||
const tenant_id = req.user.tenant_id
|
||||
const userId = authUser.user_id
|
||||
const activeTenantId = authUser.tenant_id
|
||||
|
||||
// 1. User laden
|
||||
const { data: user, error: userError } = await server.supabase
|
||||
.from("auth_users")
|
||||
.select("id, email, created_at, must_change_password")
|
||||
.eq("id", authUser.user_id)
|
||||
.single()
|
||||
// ----------------------------------------------------
|
||||
// 1) USER LADEN
|
||||
// ----------------------------------------------------
|
||||
const userResult = await server.db
|
||||
.select({
|
||||
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) {
|
||||
return reply.code(401).send({ error: "User not found" })
|
||||
}
|
||||
const user = userResult[0]
|
||||
|
||||
// 2. Tenants laden (alle Tenants des Users)
|
||||
const { data: tenantLinks, error: tenantLinksError } = await server.supabase
|
||||
.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 (!user) {
|
||||
return reply.code(401).send({ error: "User not found" })
|
||||
}
|
||||
|
||||
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
|
||||
const activeTenant = authUser.tenant_id /*|| tenants[0].id*/
|
||||
profile = profileResult?.[0] ?? null
|
||||
}
|
||||
|
||||
// 4. Profil für den aktiven Tenant laden
|
||||
let profile = null
|
||||
if (activeTenant) {
|
||||
const { data: profileData } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.eq("tenant_id", activeTenant)
|
||||
.single()
|
||||
// ----------------------------------------------------
|
||||
// 5) PERMISSIONS — RPC ERSETZT
|
||||
// ----------------------------------------------------
|
||||
const permissionRows =
|
||||
(await server.db
|
||||
.select({
|
||||
permission: authRolePermissions.permission,
|
||||
})
|
||||
.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
|
||||
.rpc("auth_get_user_permissions", {
|
||||
uid: user.id,
|
||||
tid: activeTenant || null
|
||||
})
|
||||
|
||||
if(permissionsError) {
|
||||
console.log(permissionsError)
|
||||
}
|
||||
|
||||
const permissions = permissionsData.map(i => i.permission) || []
|
||||
|
||||
// 6. Response zurückgeben
|
||||
return {
|
||||
user,
|
||||
tenants,
|
||||
activeTenant,
|
||||
profile,
|
||||
permissions
|
||||
// ----------------------------------------------------
|
||||
// RESPONSE
|
||||
// ----------------------------------------------------
|
||||
return {
|
||||
user,
|
||||
tenants: tenantList,
|
||||
activeTenant,
|
||||
profile,
|
||||
permissions,
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("ERROR in /me route:", err)
|
||||
return reply.code(500).send({ error: "Internal server error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,169 +1,31 @@
|
||||
import nodemailer from "nodemailer"
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { eq } from "drizzle-orm"
|
||||
|
||||
import { sendMailAsUser } from "../utils/emailengine"
|
||||
import { encrypt, decrypt } from "../utils/crypt"
|
||||
import { userCredentials } from "../../db/schema"
|
||||
// Pfad ggf. anpassen
|
||||
|
||||
import { FastifyInstance } from "fastify";
|
||||
import {sendMailAsUser} from "../utils/emailengine";
|
||||
import {encrypt, decrypt} from "../utils/crypt"
|
||||
import {secrets} from "../utils/secrets";
|
||||
// @ts-ignore
|
||||
import MailComposer from 'nodemailer/lib/mail-composer/index.js'
|
||||
|
||||
import {ImapFlow} from "imapflow"
|
||||
import MailComposer from "nodemailer/lib/mail-composer/index.js"
|
||||
import { ImapFlow } from "imapflow"
|
||||
|
||||
export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
|
||||
// Create E-Mail Account
|
||||
|
||||
// ======================================================================
|
||||
// CREATE OR UPDATE EMAIL ACCOUNT
|
||||
// ======================================================================
|
||||
server.post("/email/accounts/:id?", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
|
||||
const body = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
smtp_host: string
|
||||
smtp_port: number
|
||||
smtp_ssl: boolean
|
||||
imap_host: string
|
||||
imap_port: number
|
||||
imap_ssl: boolean
|
||||
};
|
||||
|
||||
if(id) {
|
||||
//SAVE Existing
|
||||
let saveData = {
|
||||
email_encrypted: body.email ? encrypt(body.email) : undefined,
|
||||
password_encrypted: body.password ? encrypt(body.password) : undefined,
|
||||
smtp_host_encrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined,
|
||||
smtp_port: body.smtp_port,
|
||||
smtp_ssl: body.smtp_ssl,
|
||||
imap_host_encrypted: body.imap_host ? encrypt(body.imap_host) : undefined,
|
||||
imap_port: body.imap_port,
|
||||
imap_ssl: body.imap_ssl,
|
||||
}
|
||||
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("user_credentials")
|
||||
.update(saveData)
|
||||
.eq("id", id)
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
} else {
|
||||
return reply.send({success: true})
|
||||
}
|
||||
} else {
|
||||
//Create New
|
||||
let createData = {
|
||||
user_id: req.user.user_id,
|
||||
email_encrypted: encrypt(body.email),
|
||||
password_encrypted: encrypt(body.password),
|
||||
tenant_id: req.user.tenant_id,
|
||||
smtp_host_encrypted: encrypt(body.smtp_host),
|
||||
smtp_port: body.smtp_port,
|
||||
smtp_ssl: body.smtp_ssl,
|
||||
type: "mail",
|
||||
imap_host_encrypted: encrypt(body.imap_host),
|
||||
imap_port: body.imap_port,
|
||||
imap_ssl: body.imap_ssl,
|
||||
}
|
||||
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("user_credentials")
|
||||
.insert(createData)
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
} else {
|
||||
return reply.send({success: true})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
server.get("/email/accounts/:id?", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string };
|
||||
|
||||
if(id) {
|
||||
let returnData = {}
|
||||
// @ts-ignore
|
||||
const { data, error } = await server.supabase
|
||||
.from("user_credentials")
|
||||
.select("id, email_encrypted, smtp_host_encrypted, smtp_port, smtp_ssl, imap_host_encrypted, imap_port, imap_ssl, user_id, tenant_id")
|
||||
.eq("id", id)
|
||||
.eq("tenant_id", req.user.tenant_id)
|
||||
.eq("type", "mail")
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return reply.code(404).send({ error: "Not found" });
|
||||
} else {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if(key.includes("encrypted")){
|
||||
returnData[key.substring(0,key.length-10)] = decrypt(data[key])
|
||||
} else {
|
||||
returnData[key] = data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return returnData;
|
||||
} else {
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("user_credentials")
|
||||
.select("id, email_encrypted, user_id, tenant_id")
|
||||
.eq("tenant_id", req.user.tenant_id)
|
||||
.eq("type", "mail")
|
||||
|
||||
let accounts = []
|
||||
data.forEach(item => {
|
||||
let temp = {}
|
||||
Object.keys(item).forEach((key) => {
|
||||
if(key.includes("encrypted")){
|
||||
temp[key.substring(0,key.length-10)] = decrypt(item[key])
|
||||
} else {
|
||||
temp[key] = item[key]
|
||||
}
|
||||
})
|
||||
accounts.push(temp)
|
||||
})
|
||||
|
||||
|
||||
|
||||
return accounts
|
||||
}
|
||||
});
|
||||
|
||||
server.post("/email/send", async (req, reply) => {
|
||||
const body = req.body as {
|
||||
to: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
attachments?: any,
|
||||
account: string
|
||||
}
|
||||
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
let accountData = {} as {
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
const body = req.body as {
|
||||
email: string
|
||||
password: string
|
||||
smtp_host: string
|
||||
@@ -173,32 +35,175 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
imap_port: number
|
||||
imap_ssl: boolean
|
||||
}
|
||||
// @ts-ignore
|
||||
const { data, error } = await server.supabase
|
||||
.from("user_credentials")
|
||||
.select("id, email_encrypted,password_encrypted, smtp_host_encrypted, smtp_port, smtp_ssl,imap_host_encrypted,imap_port, imap_ssl, user_id, tenant_id")
|
||||
.eq("id", body.account)
|
||||
.eq("tenant_id", req.user.tenant_id)
|
||||
.eq("type", "mail")
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return reply.code(404).send({ error: "Not found" });
|
||||
} else {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if(key.includes("encrypted")){
|
||||
accountData[key.substring(0,key.length-10)] = decrypt(data[key])
|
||||
} else {
|
||||
accountData[key] = data[key]
|
||||
}
|
||||
})
|
||||
// -----------------------------
|
||||
// UPDATE EXISTING
|
||||
// -----------------------------
|
||||
if (id) {
|
||||
const saveData = {
|
||||
emailEncrypted: body.email ? encrypt(body.email) : undefined,
|
||||
passwordEncrypted: body.password ? encrypt(body.password) : undefined,
|
||||
smtpHostEncrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined,
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
imapHostEncrypted: body.imap_host ? encrypt(body.imap_host) : undefined,
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
}
|
||||
|
||||
await server.db
|
||||
.update(userCredentials)
|
||||
//@ts-ignore
|
||||
.set(saveData)
|
||||
.where(eq(userCredentials.id, id))
|
||||
|
||||
return reply.send({ success: true })
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// CREATE NEW
|
||||
// -----------------------------
|
||||
const insertData = {
|
||||
userId: req.user.user_id,
|
||||
tenantId: req.user.tenant_id,
|
||||
type: "mail",
|
||||
|
||||
emailEncrypted: encrypt(body.email),
|
||||
passwordEncrypted: encrypt(body.password),
|
||||
|
||||
smtpHostEncrypted: encrypt(body.smtp_host),
|
||||
smtpPort: body.smtp_port,
|
||||
smtpSsl: body.smtp_ssl,
|
||||
|
||||
imapHostEncrypted: encrypt(body.imap_host),
|
||||
imapPort: body.imap_port,
|
||||
imapSsl: body.imap_ssl,
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
await server.db.insert(userCredentials).values(insertData)
|
||||
|
||||
return reply.send({ success: true })
|
||||
} catch (err) {
|
||||
console.error("POST /email/accounts error:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// ======================================================================
|
||||
// GET SINGLE OR ALL ACCOUNTS
|
||||
// ======================================================================
|
||||
server.get("/email/accounts/:id?", async (req, reply) => {
|
||||
try {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
// ============================================================
|
||||
// LOAD SINGLE ACCOUNT
|
||||
// ============================================================
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, id))
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
const returnData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted")) {
|
||||
const cleanKey = key.replace("Encrypted", "")
|
||||
// @ts-ignore
|
||||
returnData[cleanKey] = decrypt(val as string)
|
||||
} else {
|
||||
returnData[key] = val
|
||||
}
|
||||
})
|
||||
|
||||
return reply.send(returnData)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LOAD ALL ACCOUNTS FOR TENANT
|
||||
// ============================================================
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.tenantId, req.user.tenant_id))
|
||||
|
||||
const accounts = rows.map(row => {
|
||||
const temp: any = {}
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted")) {
|
||||
// @ts-ignore
|
||||
temp[key.replace("Encrypted", "")] = decrypt(val as string)
|
||||
} else {
|
||||
temp[key] = val
|
||||
}
|
||||
})
|
||||
return temp
|
||||
})
|
||||
|
||||
return reply.send(accounts)
|
||||
|
||||
} catch (err) {
|
||||
console.error("GET /email/accounts error:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// ======================================================================
|
||||
// SEND EMAIL + SAVE IN IMAP SENT FOLDER
|
||||
// ======================================================================
|
||||
server.post("/email/send", async (req, reply) => {
|
||||
try {
|
||||
const body = req.body as {
|
||||
to: string
|
||||
cc?: string
|
||||
bcc?: string
|
||||
subject?: string
|
||||
text?: string
|
||||
html?: string
|
||||
attachments?: any
|
||||
account: string
|
||||
}
|
||||
|
||||
// Fetch email credentials
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(userCredentials)
|
||||
.where(eq(userCredentials.id, body.account))
|
||||
|
||||
const row = rows[0]
|
||||
if (!row) return reply.code(404).send({ error: "Account not found" })
|
||||
|
||||
const accountData: any = {}
|
||||
|
||||
Object.entries(row).forEach(([key, val]) => {
|
||||
if (key.endsWith("Encrypted")) {
|
||||
// @ts-ignore
|
||||
accountData[key.replace("Encrypted", "")] = decrypt(val as string)
|
||||
} else {
|
||||
accountData[key] = val
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------
|
||||
// SEND EMAIL VIA SMTP
|
||||
// -------------------------
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: accountData.smtp_host,
|
||||
port: accountData.smtp_port,
|
||||
secure: accountData.smtp_ssl,
|
||||
host: accountData.smtpHost,
|
||||
port: accountData.smtpPort,
|
||||
secure: accountData.smtpSsl,
|
||||
auth: {
|
||||
user: accountData.email,
|
||||
pass: accountData.password,
|
||||
@@ -208,62 +213,48 @@ export default async function emailAsUserRoutes(server: FastifyInstance) {
|
||||
const message = {
|
||||
from: accountData.email,
|
||||
to: body.to,
|
||||
cc: body.cc ? body.cc : undefined,
|
||||
bcc: body.bcc ? body.bcc : undefined,
|
||||
cc: body.cc,
|
||||
bcc: body.bcc,
|
||||
subject: body.subject,
|
||||
html: body.html ? body.html : undefined,
|
||||
html: body.html,
|
||||
text: body.text,
|
||||
attachments: body.attachments ? body.attachments : undefined,
|
||||
attachments: body.attachments,
|
||||
}
|
||||
|
||||
|
||||
const info = await transporter.sendMail(message)
|
||||
|
||||
const imapClient = new ImapFlow({
|
||||
host: accountData.imap_host,
|
||||
port: accountData.imap_port,
|
||||
secure: accountData.imap_ssl,
|
||||
// -------------------------
|
||||
// SAVE TO IMAP SENT FOLDER
|
||||
// -------------------------
|
||||
const imap = new ImapFlow({
|
||||
host: accountData.imapHost,
|
||||
port: accountData.imapPort,
|
||||
secure: accountData.imapSsl,
|
||||
auth: {
|
||||
user: accountData.email,
|
||||
pass: accountData.password,
|
||||
},
|
||||
logger: false
|
||||
})
|
||||
|
||||
await imapClient.connect()
|
||||
await imap.connect()
|
||||
|
||||
const mail = new MailComposer(message)
|
||||
const raw = await mail.compile().build()
|
||||
|
||||
const raw = await mail.compile().build() // → Buffer mit kompletter MIME
|
||||
|
||||
|
||||
for await (const mailbox of await imapClient.list()) {
|
||||
// mailbox.flags enthält z. B. ['\\Sent', '\\HasChildren']
|
||||
console.log(mailbox.specialUse)
|
||||
if (mailbox.specialUse == '\\Sent') {
|
||||
console.log('📨 Sent folder gefunden:', mailbox.path)
|
||||
await imapClient.mailboxOpen(mailbox.path)
|
||||
|
||||
await imapClient.append(mailbox.path, raw, ['\\Seen'])
|
||||
|
||||
await imapClient.logout()
|
||||
|
||||
break
|
||||
for await (const mailbox of await imap.list()) {
|
||||
if (mailbox.specialUse === "\\Sent") {
|
||||
await imap.mailboxOpen(mailbox.path)
|
||||
await imap.append(mailbox.path, raw, ["\\Seen"])
|
||||
await imap.logout()
|
||||
}
|
||||
}
|
||||
|
||||
if(info.response.includes("OK")){
|
||||
reply.send({success: true})
|
||||
}{
|
||||
reply.status(500)
|
||||
}
|
||||
|
||||
return reply.send({ success: true })
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
reply.code(500).send({ error: "Failed to send E-Mail as User" })
|
||||
console.error("POST /email/send error:", err)
|
||||
return reply.code(500).send({ error: "Failed to send email" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,15 @@ import {getSignedUrl} from "@aws-sdk/s3-request-presigner";
|
||||
import dayjs from "dayjs";
|
||||
import {randomUUID} from "node:crypto";
|
||||
import {secrets} from "../utils/secrets";
|
||||
import {createSEPAExport} from "../utils/export/sepa";
|
||||
|
||||
const createExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => {
|
||||
const createDatevExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => {
|
||||
console.log(startDate,endDate,beraternr,mandantennr)
|
||||
|
||||
// 1) ZIP erzeugen
|
||||
const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr)
|
||||
console.log("ZIP created")
|
||||
console.log(buffer)
|
||||
|
||||
// 2) Dateiname & Key festlegen
|
||||
const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip`
|
||||
@@ -80,7 +82,27 @@ export default async function exportRoutes(server: FastifyInstance) {
|
||||
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await createExport(server,req,start_date,end_date,beraternr,mandantennr)
|
||||
await createDatevExport(server,req,start_date,end_date,beraternr,mandantennr)
|
||||
console.log("Job done ✅")
|
||||
} catch (err) {
|
||||
console.error("Job failed ❌", err)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
server.post("/exports/sepa", async (req, reply) => {
|
||||
const { idsToExport } = req.body as {
|
||||
idsToExport: Array<number>
|
||||
}
|
||||
|
||||
|
||||
|
||||
reply.send({success:true})
|
||||
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await createSEPAExport(server, idsToExport, req.user.tenant_id)
|
||||
console.log("Job done ✅")
|
||||
} catch (err) {
|
||||
console.error("Job failed ❌", err)
|
||||
|
||||
@@ -1,184 +1,202 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import multipart from "@fastify/multipart"
|
||||
import { s3 } from "../utils/s3"
|
||||
import {GetObjectCommand, PutObjectCommand} from "@aws-sdk/client-s3"
|
||||
import {getSignedUrl} from "@aws-sdk/s3-request-presigner";
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand
|
||||
} from "@aws-sdk/client-s3"
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
|
||||
import archiver from "archiver"
|
||||
import {secrets} from "../utils/secrets"
|
||||
import { secrets } from "../utils/secrets"
|
||||
|
||||
import { eq, inArray } from "drizzle-orm"
|
||||
import {
|
||||
files,
|
||||
createddocuments,
|
||||
customers
|
||||
} from "../../db/schema"
|
||||
|
||||
|
||||
export default async function fileRoutes(server: FastifyInstance) {
|
||||
await server.register(multipart,{
|
||||
limits: {
|
||||
fileSize: 20 * 1024 * 1024, // 20 MB
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MULTIPART INIT
|
||||
// -------------------------------------------------------------
|
||||
await server.register(multipart, {
|
||||
limits: { fileSize: 20 * 1024 * 1024 } // 20 MB
|
||||
})
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// UPLOAD FILE
|
||||
// -------------------------------------------------------------
|
||||
server.post("/files/upload", async (req, reply) => {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const data:any = await req.file()
|
||||
const fileBuffer = await data.toBuffer()
|
||||
const data: any = await req.file()
|
||||
if (!data?.file) return reply.code(400).send({ error: "No file uploaded" })
|
||||
const fileBuffer = await data.toBuffer()
|
||||
|
||||
console.log(data)
|
||||
const meta = data.fields?.meta?.value ? JSON.parse(data.fields.meta.value) : {}
|
||||
|
||||
// 1️⃣ DB-Eintrag erzeugen
|
||||
const inserted = await server.db
|
||||
.insert(files)
|
||||
.values({ tenant: tenantId })
|
||||
.returning()
|
||||
|
||||
let meta = JSON.parse(data.fields?.meta?.value)
|
||||
const created = inserted[0]
|
||||
if (!created) throw new Error("Could not create DB entry")
|
||||
|
||||
if (!data.file) return reply.code(400).send({ error: "No file uploaded" })
|
||||
|
||||
|
||||
const {data:createdFileData,error:createdFileError} = await server.supabase
|
||||
.from("files")
|
||||
.insert({
|
||||
tenant: tenantId,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if(createdFileError) {
|
||||
console.log(createdFileError)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
} else if(createdFileData && data.file) {
|
||||
const fileKey = `${tenantId}/filesbyid/${createdFileData.id}/${data.filename}`
|
||||
// 2️⃣ Datei in S3 speichern
|
||||
const fileKey = `${tenantId}/filesbyid/${created.id}/${data.filename}`
|
||||
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: fileKey,
|
||||
Body: fileBuffer,
|
||||
ContentType: data.mimetype,
|
||||
ContentType: data.mimetype
|
||||
}))
|
||||
|
||||
//Update File with Corresponding Path
|
||||
const {data:updateFileData, error:updateFileError} = await server.supabase
|
||||
.from("files")
|
||||
.update({
|
||||
// 3️⃣ DB updaten: meta + path
|
||||
await server.db
|
||||
.update(files)
|
||||
.set({
|
||||
...meta,
|
||||
path: fileKey,
|
||||
path: fileKey
|
||||
})
|
||||
.eq("id", createdFileData.id)
|
||||
|
||||
if(updateFileError) {
|
||||
console.log(updateFileError)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
|
||||
} else {
|
||||
/*const {data:tagData, error:tagError} = await server.supabase
|
||||
.from("filetagmembers")
|
||||
.insert(tags.map(tag => {
|
||||
return {
|
||||
file_id: createdFileData.id,
|
||||
tag_id: tag
|
||||
}
|
||||
}))*/
|
||||
|
||||
return { id: createdFileData.id, filename: data.filename, path: fileKey }
|
||||
.where(eq(files.id, created.id))
|
||||
|
||||
return {
|
||||
id: created.id,
|
||||
filename: data.filename,
|
||||
path: fileKey
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Upload failed" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET FILE OR LIST FILES
|
||||
// -------------------------------------------------------------
|
||||
server.get("/files/:id?", async (req, reply) => {
|
||||
const { id } = req.params as { id?: string }
|
||||
try {
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
if(id) {
|
||||
try {
|
||||
const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single()
|
||||
// 🔹 EINZELNE DATEI
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
return {...data}
|
||||
} catch (err) {
|
||||
req.log.error(err);
|
||||
reply.code(500).send({ error: "Could not generate presigned URL" });
|
||||
const file = rows[0]
|
||||
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
return file
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id)
|
||||
|
||||
// 🔹 ALLE DATEIEN DES TENANTS (mit createddocument + customer)
|
||||
const tenantId = req.user?.tenant_id
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const list = await server.db
|
||||
//@ts-ignore
|
||||
.select({
|
||||
...files,
|
||||
createddocument: createddocuments,
|
||||
customer: customers
|
||||
})
|
||||
.from(files)
|
||||
.leftJoin(
|
||||
createddocuments,
|
||||
eq(files.createddocument, createddocuments.id)
|
||||
)
|
||||
.leftJoin(
|
||||
customers,
|
||||
eq(createddocuments.customer, customers.id)
|
||||
)
|
||||
.where(eq(files.tenant, tenantId))
|
||||
|
||||
return { files: supabaseFileEntries }
|
||||
} catch (err) {
|
||||
req.log.error(err)
|
||||
reply.code(500).send({ error: "Could not generate presigned URLs" })
|
||||
}
|
||||
return { files: list }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Could not load files" })
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DOWNLOAD (SINGLE OR MULTI ZIP)
|
||||
// -------------------------------------------------------------
|
||||
server.post("/files/download/:id?", async (req, reply) => {
|
||||
const { id } = req.params as { id?: string }
|
||||
|
||||
// @ts-ignore
|
||||
const ids = req.body?.ids || []
|
||||
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
// 🔹 Einzeldownload
|
||||
const { data, error } = await server.supabase
|
||||
.from("files")
|
||||
.select("*")
|
||||
.eq("id", id)
|
||||
.single()
|
||||
const { id } = req.params as { id?: string }
|
||||
//@ts-ignore
|
||||
const ids = req.body?.ids || []
|
||||
|
||||
if (error || !data) {
|
||||
return reply.code(404).send({ error: "File not found" })
|
||||
}
|
||||
// -------------------------------------------------
|
||||
// 1️⃣ SINGLE DOWNLOAD
|
||||
// -------------------------------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
const file = rows[0]
|
||||
if (!file) return reply.code(404).send({ error: "File not found" })
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: data.path,
|
||||
Key: file.path!
|
||||
})
|
||||
|
||||
const { Body, ContentType } = await s3.send(command)
|
||||
|
||||
const chunks: any[] = []
|
||||
// @ts-ignore
|
||||
for await (const chunk of Body) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
for await (const chunk of Body as any) chunks.push(chunk)
|
||||
const buffer = Buffer.concat(chunks)
|
||||
|
||||
reply.header("Content-Type", ContentType || "application/octet-stream")
|
||||
reply.header(
|
||||
"Content-Disposition",
|
||||
`attachment; filename="${data.path.split("/").pop()}"`
|
||||
)
|
||||
reply.header("Content-Disposition", `attachment; filename="${file.path?.split("/").pop()}"`)
|
||||
return reply.send(buffer)
|
||||
}
|
||||
|
||||
console.log(ids)
|
||||
|
||||
// -------------------------------------------------
|
||||
// 2️⃣ MULTI DOWNLOAD → ZIP
|
||||
// -------------------------------------------------
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
// 🔹 Multi-Download → ZIP zurückgeben
|
||||
const { data: supabaseFiles, error } = await server.supabase
|
||||
.from("files")
|
||||
.select("*")
|
||||
.in("id", ids)
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(inArray(files.id, ids))
|
||||
|
||||
if (error || !supabaseFiles?.length) {
|
||||
return reply.code(404).send({ error: "Files not found" })
|
||||
}
|
||||
|
||||
console.log(supabaseFiles)
|
||||
if (!rows.length) return reply.code(404).send({ error: "Files not found" })
|
||||
|
||||
reply.header("Content-Type", "application/zip")
|
||||
reply.header("Content-Disposition", "attachment; filename=dateien.zip")
|
||||
reply.header("Content-Disposition", `attachment; filename="dateien.zip"`)
|
||||
|
||||
const archive = archiver("zip", { zlib: { level: 9 } })
|
||||
archive.on("warning", console.warn)
|
||||
|
||||
for (const entry of supabaseFiles) {
|
||||
const command = new GetObjectCommand({
|
||||
for (const entry of rows) {
|
||||
const cmd = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: entry.path,
|
||||
Key: entry.path!
|
||||
})
|
||||
const { Body } = await s3.send(cmd)
|
||||
|
||||
const { Body } = await s3.send(command)
|
||||
const filename = entry.path.split("/").pop() || entry.id
|
||||
console.log(filename)
|
||||
archive.append(Body as any, { name: filename })
|
||||
archive.append(Body as any, {
|
||||
name: entry.path?.split("/").pop() || entry.id
|
||||
})
|
||||
}
|
||||
|
||||
await archive.finalize()
|
||||
@@ -186,80 +204,90 @@ export default async function fileRoutes(server: FastifyInstance) {
|
||||
}
|
||||
|
||||
return reply.code(400).send({ error: "No id or ids provided" })
|
||||
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
reply.code(500).send({ error: "Download failed" })
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Download failed" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GENERATE PRESIGNED URL(S)
|
||||
// -------------------------------------------------------------
|
||||
server.post("/files/presigned/:id?", async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const { ids } = req.body as { ids: string[] }
|
||||
try {
|
||||
const { id } = req.params as { id?: string }
|
||||
const { ids } = req.body as { ids?: string[] }
|
||||
const tenantId = req.user?.tenant_id
|
||||
|
||||
if(id) {
|
||||
try {
|
||||
const {data,error} = await server.supabase.from("files").select("*").eq("id", id).single()
|
||||
if (!tenantId) return reply.code(401).send({ error: "Unauthorized" })
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: data.path,
|
||||
});
|
||||
// -------------------------------------------------
|
||||
// SINGLE FILE PRESIGNED URL
|
||||
// -------------------------------------------------
|
||||
if (id) {
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.id, id))
|
||||
|
||||
// URL für 15 Minuten gültig
|
||||
const url = await getSignedUrl(s3, command, { expiresIn: 900 });
|
||||
const file = rows[0]
|
||||
if (!file) return reply.code(404).send({ error: "Not found" })
|
||||
|
||||
return { ...data, url };
|
||||
} catch (err) {
|
||||
req.log.error(err);
|
||||
reply.code(500).send({ error: "Could not generate presigned URL" });
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return reply.code(400).send({ error: "No file keys provided" })
|
||||
}
|
||||
const url = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
|
||||
{ expiresIn: 900 }
|
||||
)
|
||||
|
||||
try {
|
||||
const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").eq("tenant",req.user.tenant_id).is("archived",false)
|
||||
return { ...file, url }
|
||||
} else {
|
||||
// -------------------------------------------------
|
||||
// MULTIPLE PRESIGNED URLs
|
||||
// -------------------------------------------------
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return reply.code(400).send({ error: "No ids provided" })
|
||||
}
|
||||
|
||||
console.log(error)
|
||||
const rows = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(eq(files.tenant, tenantId))
|
||||
|
||||
let filteredFiles = supabaseFileEntries.filter(i => ids.includes(i.id))
|
||||
filteredFiles = filteredFiles.filter(i => i.path)
|
||||
const selected = rows.filter(f => ids.includes(f.id) && f.path)
|
||||
|
||||
console.log(filteredFiles.filter(i => !i.path))
|
||||
console.log(selected)
|
||||
|
||||
const url = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: selected[0].path! }),
|
||||
{ expiresIn: 900 }
|
||||
)
|
||||
console.log(url)
|
||||
console.log(selected.filter(f => !f.path))
|
||||
|
||||
|
||||
let urls = await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
let file = filteredFiles.find(i => i.id === id)
|
||||
|
||||
if(!file) return
|
||||
|
||||
let key = file.path
|
||||
if(!key) console.log(file)
|
||||
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: key,
|
||||
})
|
||||
|
||||
const url = await getSignedUrl(s3, command, { expiresIn: 900 }) // 15 min gültig
|
||||
|
||||
|
||||
return {...filteredFiles.find(i => i.id === id), url}
|
||||
const output = await Promise.all(
|
||||
selected.map(async (file) => {
|
||||
const url = await getSignedUrl(
|
||||
s3,
|
||||
new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }),
|
||||
{ expiresIn: 900 }
|
||||
)
|
||||
return { ...file, url }
|
||||
})
|
||||
)
|
||||
|
||||
urls = urls.filter(i => i)
|
||||
|
||||
return { files: urls }
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
reply.code(500).send({ error: "Could not generate presigned URLs" })
|
||||
return { files: output }
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return reply.code(500).send({ error: "Could not create presigned URLs" })
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf";
|
||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||
//import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions";
|
||||
import dayjs from "dayjs";
|
||||
//import { ready as zplReady } from 'zpl-renderer-js'
|
||||
//import { renderZPL } from "zpl-image";
|
||||
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat.js";
|
||||
import isoWeek from "dayjs/plugin/isoWeek.js";
|
||||
@@ -11,6 +13,9 @@ import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"
|
||||
import duration from "dayjs/plugin/duration.js";
|
||||
import timezone from "dayjs/plugin/timezone.js";
|
||||
import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
||||
import {citys} from "../../db/schema";
|
||||
import {eq} from "drizzle-orm";
|
||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(isoWeek)
|
||||
dayjs.extend(isBetween)
|
||||
@@ -48,8 +53,6 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
)
|
||||
}
|
||||
|
||||
console.log(pdf)
|
||||
|
||||
return pdf // Fastify wandelt automatisch in JSON
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
@@ -102,7 +105,11 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await server.supabase
|
||||
//@ts-ignore
|
||||
const data = await server.db.select().from(citys).where(eq(citys.zip,zip))
|
||||
|
||||
|
||||
/*const { data, error } = await server.supabase
|
||||
.from('citys')
|
||||
.select()
|
||||
.eq('zip', zip)
|
||||
@@ -111,7 +118,7 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return reply.code(500).send({ error: 'Database error' })
|
||||
}
|
||||
}*/
|
||||
|
||||
if (!data) {
|
||||
return reply.code(404).send({ error: 'ZIP not found' })
|
||||
@@ -141,6 +148,7 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
|
||||
return reply.send({
|
||||
...data,
|
||||
//@ts-ignore
|
||||
state_code: bundeslaender.find(i => i.name === data.countryName)
|
||||
})
|
||||
} catch (err) {
|
||||
@@ -149,4 +157,43 @@ export default async function functionRoutes(server: FastifyInstance) {
|
||||
}
|
||||
})
|
||||
|
||||
/*server.post('/print/zpl/preview', async (req, reply) => {
|
||||
const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string}
|
||||
|
||||
console.log(widthMm,heightMm,dpmm)
|
||||
|
||||
if (!zpl) {
|
||||
return reply.code(400).send({ error: 'Missing ZPL string' })
|
||||
}
|
||||
|
||||
try {
|
||||
// 1️⃣ Renderer initialisieren
|
||||
const { api } = await zplReady
|
||||
|
||||
// 2️⃣ Rendern (liefert base64-encoded PNG)
|
||||
const base64Png = await api.zplToBase64Async(zpl, widthMm, heightMm, dpmm)
|
||||
|
||||
return await encodeBase64ToNiimbot(base64Png, 'top')
|
||||
} catch (err) {
|
||||
console.error('[ZPL Preview Error]', err)
|
||||
return reply.code(500).send({ error: err.message || 'Failed to render ZPL' })
|
||||
}
|
||||
})
|
||||
|
||||
server.post('/print/label', async (req, reply) => {
|
||||
const { context, width=584, heigth=354 } = req.body as {context:any,width:number,heigth:number}
|
||||
|
||||
try {
|
||||
const base64 = await generateLabel(context,width,heigth)
|
||||
|
||||
return {
|
||||
encoded: await encodeBase64ToNiimbot(base64, 'top'),
|
||||
base64: base64
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ZPL Preview Error]', err)
|
||||
return reply.code(500).send({ error: err.message || 'Failed to render ZPL' })
|
||||
}
|
||||
})*/
|
||||
|
||||
}
|
||||
@@ -1,54 +1,120 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
import {
|
||||
authProfiles,
|
||||
} from "../../db/schema";
|
||||
|
||||
export default async function authProfilesRoutes(server: FastifyInstance) {
|
||||
// Ein einzelnes Profil laden (nur im aktuellen Tenant)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET SINGLE PROFILE
|
||||
// -------------------------------------------------------------
|
||||
server.get("/profiles/:id", async (req, reply) => {
|
||||
const { id } = req.params as {id:string};
|
||||
const tenantId = (req.user as any)?.tenant_id;
|
||||
try {
|
||||
const { id } = req.params as { id: string };
|
||||
const tenantId = (req.user as any)?.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 rows = await server.db
|
||||
.select()
|
||||
.from(authProfiles)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.id, id),
|
||||
eq(authProfiles.tenant_id, tenantId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!rows.length) {
|
||||
return reply.code(404).send({ error: "User not found or not in tenant" });
|
||||
}
|
||||
|
||||
return rows[0];
|
||||
|
||||
} catch (error) {
|
||||
console.error("GET /profiles/:id ERROR:", error);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.select()
|
||||
.eq("id", id)
|
||||
.eq("tenant_id", tenantId)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
console.log(error)
|
||||
return reply.code(404).send({ error: "User not found or not in tenant" });
|
||||
}
|
||||
|
||||
console.log(data);
|
||||
|
||||
reply.send(data)
|
||||
});
|
||||
|
||||
server.put("/profiles/:id", async (req, reply) => {
|
||||
if (!req.user.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
function sanitizeProfileUpdate(body: any) {
|
||||
const cleaned: any = { ...body }
|
||||
|
||||
// ❌ Systemfelder entfernen
|
||||
const forbidden = [
|
||||
"id", "user_id", "tenant_id", "created_at", "updated_at",
|
||||
"updatedAt", "updatedBy", "old_profile_id", "full_name"
|
||||
]
|
||||
forbidden.forEach(f => delete cleaned[f])
|
||||
|
||||
// ❌ Falls NULL Strings vorkommen → in null umwandeln
|
||||
for (const key of Object.keys(cleaned)) {
|
||||
if (cleaned[key] === "") cleaned[key] = null
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string };
|
||||
const body = req.body as any
|
||||
// ✅ Date-Felder sauber konvertieren, falls vorhanden
|
||||
const dateFields = ["birthday", "entry_date"]
|
||||
|
||||
delete body.full_name
|
||||
for (const field of dateFields) {
|
||||
if (cleaned[field]) {
|
||||
const d = new Date(cleaned[field])
|
||||
if (!isNaN(d.getTime())) cleaned[field] = d
|
||||
else delete cleaned[field] // invalid → entfernen
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from("auth_profiles")
|
||||
.update(body)
|
||||
.eq("id", id)
|
||||
.eq("tenant_id", req.user.tenant_id)
|
||||
.select("*")
|
||||
.single();
|
||||
// -------------------------------------------------------------
|
||||
// UPDATE PROFILE
|
||||
// -------------------------------------------------------------
|
||||
server.put("/profiles/:id", async (req, reply) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id
|
||||
const userId = req.user?.user_id
|
||||
|
||||
if (error || !data) {
|
||||
console.log(error)
|
||||
return reply.code(404).send({ error: "User not found or not in tenant" });
|
||||
if (!tenantId) {
|
||||
return reply.code(400).send({ error: "No tenant selected" })
|
||||
}
|
||||
|
||||
const { id } = req.params as { id: string }
|
||||
let body = req.body as any
|
||||
|
||||
// Clean + Normalize
|
||||
body = sanitizeProfileUpdate(body)
|
||||
|
||||
const updateData = {
|
||||
...body,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: userId
|
||||
}
|
||||
|
||||
const updated = await server.db
|
||||
.update(authProfiles)
|
||||
.set(updateData)
|
||||
.where(
|
||||
and(
|
||||
eq(authProfiles.id, id),
|
||||
eq(authProfiles.tenant_id, tenantId)
|
||||
)
|
||||
)
|
||||
.returning()
|
||||
|
||||
if (!updated.length) {
|
||||
return reply.code(404).send({ error: "User not found or not in tenant" })
|
||||
}
|
||||
|
||||
return updated[0]
|
||||
|
||||
} catch (err) {
|
||||
console.error("PUT /profiles/:id ERROR:", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,829 +0,0 @@
|
||||
import {FastifyInstance} from "fastify";
|
||||
import {insertHistoryItem} from "../utils/history"
|
||||
import {diffObjects} from "../utils/diff";
|
||||
import {sortData} from "../utils/sort";
|
||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||
import {compareValues, getNestedValue} from "../utils/helpers";
|
||||
|
||||
|
||||
const dataTypes: any[] = {
|
||||
// @ts-ignore
|
||||
tasks: {
|
||||
isArchivable: true,
|
||||
label: "Aufgaben",
|
||||
labelSingle: "Aufgabe",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "task",
|
||||
supabaseSelectWithInformation: "*, plant(*), project(*), customer(*)",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Zuweisungen"
|
||||
],
|
||||
showTabs: [{label: 'Informationen'}]
|
||||
},
|
||||
customers: {
|
||||
isArchivable: true,
|
||||
label: "Kunden",
|
||||
labelSingle: "Kunde",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
numberRangeHolder: "customerNumber",
|
||||
historyItemHolder: "customer",
|
||||
supabaseSortColumn: "customerNumber",
|
||||
supabaseSelectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*)",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Kontaktdaten"
|
||||
],
|
||||
showTabs: [{label: 'Informationen'}, {label: 'Ansprechpartner'}, {label: 'Dateien'}, {label: 'Ausgangsbelege'}, {label: 'Projekte'}, {label: 'Objekte'}, {label: 'Termine'}, {label: 'Verträge'}]
|
||||
},
|
||||
contacts: {
|
||||
isArchivable: true,
|
||||
label: "Kontakte",
|
||||
labelSingle: "Kontakt",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "contact",
|
||||
supabaseSelectWithInformation: "*, customer(*), vendor(*)",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
contracts: {
|
||||
isArchivable: true,
|
||||
label: "Verträge",
|
||||
labelSingle: "Vertrag",
|
||||
isStandardEntity: true,
|
||||
numberRangeHolder: "contractNumber",
|
||||
redirect: true,
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Abrechnung"
|
||||
],
|
||||
supabaseSelectWithInformation: "*, customer(*), files(*)",
|
||||
showTabs: [{label: 'Informationen'}, {label: 'Dateien'}]
|
||||
},
|
||||
absencerequests: {
|
||||
isArchivable: true,
|
||||
label: "Abwesenheiten",
|
||||
labelSingle: "Abwesenheit",
|
||||
isStandardEntity: true,
|
||||
supabaseSortColumn: "startDate",
|
||||
supabaseSortAscending: false,
|
||||
supabaseSelectWithInformation: "*",
|
||||
historyItemHolder: "absencerequest",
|
||||
redirect: true,
|
||||
showTabs: [{label: 'Informationen'}]
|
||||
},
|
||||
plants: {
|
||||
isArchivable: true,
|
||||
label: "Objekte",
|
||||
labelSingle: "Objekt",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "plant",
|
||||
supabaseSelectWithInformation: "*, customer(id,name)",
|
||||
showTabs: [
|
||||
{
|
||||
label: "Informationen"
|
||||
}, {
|
||||
label: "Projekte"
|
||||
}, {
|
||||
label: "Aufgaben"
|
||||
}, {
|
||||
label: "Dateien"
|
||||
}]
|
||||
},
|
||||
products: {
|
||||
isArchivable: true,
|
||||
label: "Artikel",
|
||||
labelSingle: "Artikel",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
supabaseSelectWithInformation: "*, unit(name)",
|
||||
historyItemHolder: "product",
|
||||
showTabs: [
|
||||
{
|
||||
label: "Informationen"
|
||||
}
|
||||
]
|
||||
},
|
||||
projects: {
|
||||
isArchivable: true,
|
||||
label: "Projekte",
|
||||
labelSingle: "Projekt",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "project",
|
||||
numberRangeHolder: "projectNumber",
|
||||
supabaseSelectWithInformation: "*, customer(id,name), plant(id,name), projecttype(name, id), tasks(*, project(id,name), customer(id,name), plant(id,name)), files(*), createddocuments(*, statementallocations(*)), events(*), times(*, profile(id, fullName))",
|
||||
supabaseSortColumn: "projectNumber",
|
||||
showTabs: [
|
||||
{
|
||||
key: "information",
|
||||
label: "Informationen"
|
||||
},
|
||||
{
|
||||
key: "phases",
|
||||
label: "Phasen"
|
||||
}, {
|
||||
key: "tasks",
|
||||
label: "Aufgaben"
|
||||
}, {
|
||||
key: "files",
|
||||
label: "Dateien"
|
||||
}, {
|
||||
label: "Zeiten"
|
||||
}, {
|
||||
label: "Ausgangsbelege"
|
||||
}, {
|
||||
label: "Termine"
|
||||
}/*,{
|
||||
key: "timetracking",
|
||||
label: "Zeiterfassung"
|
||||
},{
|
||||
key: "events",
|
||||
label: "Termine"
|
||||
},{
|
||||
key: "material",
|
||||
label: "Material"
|
||||
}*/]
|
||||
},
|
||||
vehicles: {
|
||||
isArchivable: true,
|
||||
label: "Fahrzeuge",
|
||||
labelSingle: "Fahrzeug",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "vehicle",
|
||||
supabaseSelectWithInformation: "*, checks(*), files(*)",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}, {
|
||||
label: 'Überprüfungen',
|
||||
}
|
||||
]
|
||||
},
|
||||
vendors: {
|
||||
isArchivable: true,
|
||||
label: "Lieferanten",
|
||||
labelSingle: "Lieferant",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
numberRangeHolder: "vendorNumber",
|
||||
historyItemHolder: "vendor",
|
||||
supabaseSortColumn: "vendorNumber",
|
||||
supabaseSelectWithInformation: "*, contacts(*)",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Ansprechpartner',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}
|
||||
]
|
||||
},
|
||||
messages: {
|
||||
label: "Nachrichten",
|
||||
labelSingle: "Nachricht"
|
||||
},
|
||||
spaces: {
|
||||
isArchivable: true,
|
||||
label: "Lagerplätze",
|
||||
labelSingle: "Lagerplatz",
|
||||
isStandardEntity: true,
|
||||
supabaseSelectWithInformation: "*, files(*)",
|
||||
supabaseSortColumn: "spaceNumber",
|
||||
redirect: true,
|
||||
numberRangeHolder: "spaceNumber",
|
||||
historyItemHolder: "space",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Ort"
|
||||
],
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}, {label: 'Inventarartikel'}
|
||||
]
|
||||
},
|
||||
users: {
|
||||
label: "Benutzer",
|
||||
labelSingle: "Benutzer"
|
||||
},
|
||||
createddocuments: {
|
||||
isArchivable: true,
|
||||
label: "Dokumente",
|
||||
labelSingle: "Dokument",
|
||||
supabaseSelectWithInformation: "*, files(*), statementallocations(*)",
|
||||
},
|
||||
files: {
|
||||
isArchivable: true,
|
||||
label: "Dateien",
|
||||
labelSingle: "Datei",
|
||||
supabaseSelectWithInformation: "*",
|
||||
},
|
||||
folders: {
|
||||
isArchivable: true,
|
||||
label: "Ordner",
|
||||
labelSingle: "Ordner",
|
||||
supabaseSelectWithInformation: "*",
|
||||
},
|
||||
incominginvoices: {
|
||||
label: "Eingangsrechnungen",
|
||||
labelSingle: "Eingangsrechnung",
|
||||
redirect: true
|
||||
},
|
||||
inventoryitems: {
|
||||
isArchivable: true,
|
||||
label: "Inventarartikel",
|
||||
labelSingle: "Inventarartikel",
|
||||
isStandardEntity: true,
|
||||
supabaseSelectWithInformation: "*, files(*), vendor(id,name), currentSpace(id,name)",
|
||||
redirect: true,
|
||||
numberRangeHolder: "articleNumber",
|
||||
historyItemHolder: "inventoryitem",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
"Anschaffung"
|
||||
],
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}
|
||||
]
|
||||
},
|
||||
inventoryitemgroups: {
|
||||
isArchivable: true,
|
||||
label: "Inventarartikelgruppen",
|
||||
labelSingle: "Inventarartikelgruppe",
|
||||
isStandardEntity: true,
|
||||
historyItemHolder: "inventoryitemgroup",
|
||||
supabaseSelectWithInformation: "*",
|
||||
redirect: true,
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
documentboxes: {
|
||||
isArchivable: true,
|
||||
label: "Dokumentenboxen",
|
||||
labelSingle: "Dokumentenbox",
|
||||
isStandardEntity: true,
|
||||
supabaseSelectWithInformation: "*, space(*), files(*)",
|
||||
redirect: true,
|
||||
numberRangeHolder: "key",
|
||||
historyItemHolder: "documentbox",
|
||||
inputColumns: [
|
||||
"Allgemeines",
|
||||
],
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {
|
||||
label: 'Dateien',
|
||||
}
|
||||
]
|
||||
},
|
||||
services: {
|
||||
isArchivable: true,
|
||||
label: "Leistungen",
|
||||
labelSingle: "Leistung",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
supabaseSelectWithInformation: "*, unit(*)",
|
||||
historyItemHolder: "service",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
hourrates: {
|
||||
isArchivable: true,
|
||||
label: "Stundensätze",
|
||||
labelSingle: "Stundensatz",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
supabaseSelectWithInformation: "*",
|
||||
historyItemHolder: "hourrate",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
events: {
|
||||
isArchivable: true,
|
||||
label: "Termine",
|
||||
labelSingle: "Termin",
|
||||
isStandardEntity: true,
|
||||
historyItemHolder: "event",
|
||||
supabaseSelectWithInformation: "*, project(id,name), customer(*)",
|
||||
redirect: true,
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
profiles: {
|
||||
label: "Mitarbeiter",
|
||||
labelSingle: "Mitarbeiter",
|
||||
redirect: true,
|
||||
historyItemHolder: "profile"
|
||||
},
|
||||
workingtimes: {
|
||||
isArchivable: true,
|
||||
label: "Anwesenheiten",
|
||||
labelSingle: "Anwesenheit",
|
||||
redirect: true,
|
||||
redirectToList: true
|
||||
},
|
||||
texttemplates: {
|
||||
isArchivable: true,
|
||||
label: "Textvorlagen",
|
||||
labelSingle: "Textvorlage"
|
||||
},
|
||||
bankstatements: {
|
||||
isArchivable: true,
|
||||
label: "Kontobewegungen",
|
||||
labelSingle: "Kontobewegung",
|
||||
historyItemHolder: "bankStatement",
|
||||
},
|
||||
statementallocations: {
|
||||
label: "Bankzuweisungen",
|
||||
labelSingle: "Bankzuweisung"
|
||||
},
|
||||
productcategories: {
|
||||
isArchivable: true,
|
||||
label: "Artikelkategorien",
|
||||
labelSingle: "Artikelkategorie",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
supabaseSelectWithInformation: "*",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
servicecategories: {
|
||||
isArchivable: true,
|
||||
label: "Leistungskategorien",
|
||||
labelSingle: "Leistungskategorie",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
supabaseSelectWithInformation: "*",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}
|
||||
]
|
||||
},
|
||||
trackingtrips: {
|
||||
label: "Fahrten",
|
||||
labelSingle: "Fahrt",
|
||||
redirect: true,
|
||||
historyItemHolder: "trackingtrip",
|
||||
},
|
||||
projecttypes: {
|
||||
isArchivable: true,
|
||||
label: "Projekttypen",
|
||||
labelSingle: "Projekttyp",
|
||||
redirect: true,
|
||||
historyItemHolder: "projecttype"
|
||||
},
|
||||
checks: {
|
||||
isArchivable: true,
|
||||
label: "Überprüfungen",
|
||||
labelSingle: "Überprüfung",
|
||||
isStandardEntity: true,
|
||||
supabaseSelectWithInformation: "*, vehicle(id,licensePlate), profile(id, fullName), inventoryitem(name), files(*)",
|
||||
redirect: true,
|
||||
historyItemHolder: "check",
|
||||
showTabs: [
|
||||
{
|
||||
label: 'Informationen',
|
||||
}, {label: 'Dateien'}, {label: 'Ausführungen'}]
|
||||
},
|
||||
roles: {
|
||||
label: "Rollen",
|
||||
labelSingle: "Rolle",
|
||||
redirect: true,
|
||||
historyItemHolder: "role",
|
||||
filters: [],
|
||||
templateColumns: [
|
||||
{
|
||||
key: "name",
|
||||
label: "Name"
|
||||
}, {
|
||||
key: "description",
|
||||
label: "Beschreibung"
|
||||
}
|
||||
]
|
||||
},
|
||||
costcentres: {
|
||||
isArchivable: true,
|
||||
label: "Kostenstellen",
|
||||
labelSingle: "Kostenstelle",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
numberRangeHolder: "number",
|
||||
historyItemHolder: "costcentre",
|
||||
supabaseSortColumn: "number",
|
||||
supabaseSelectWithInformation: "*, project(*), vehicle(*), inventoryitem(*)",
|
||||
showTabs: [{label: 'Informationen'}, {label: 'Auswertung Kostenstelle'}]
|
||||
},
|
||||
ownaccounts: {
|
||||
isArchivable: true,
|
||||
label: "zusätzliche Buchungskonten",
|
||||
labelSingle: "zusätzliches Buchungskonto",
|
||||
isStandardEntity: true,
|
||||
redirect: true,
|
||||
historyItemHolder: "ownaccount",
|
||||
supabaseSortColumn: "number",
|
||||
supabaseSelectWithInformation: "*, statementallocations(*, bs_id(*))",
|
||||
showTabs: [{label: 'Informationen'}, {label: 'Buchungen'}]
|
||||
},
|
||||
tickets: {
|
||||
isArchivable: true,
|
||||
label: "Tickets",
|
||||
labelSingle: "Ticket",
|
||||
|
||||
},
|
||||
ticketmessages: {
|
||||
isArchivable: true,
|
||||
label: "Nachrichten",
|
||||
labelSingle: "Nachricht",
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
export default async function resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
//Liste
|
||||
server.get("/resource/:resource", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const { resource } = req.params as { resource: string };
|
||||
|
||||
const {select, sort, asc } = req.query as { select?: string, sort?: string, asc?: string }
|
||||
console.log(select, sort, asc)
|
||||
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from(resource)
|
||||
//@ts-ignore
|
||||
.select(select || dataTypes[resource].supabaseSelectWithInformation)
|
||||
.eq("tenant", req.user.tenant_id)
|
||||
if (error) {
|
||||
console.log(error)
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
const sorted =sortData(data,sort,asc === "true" ? true : false)
|
||||
|
||||
return sorted;
|
||||
});
|
||||
|
||||
|
||||
// Helper Funktionen
|
||||
|
||||
|
||||
// Liste Paginated
|
||||
server.get("/resource/:resource/paginated", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const { resource } = req.params as { resource: string };
|
||||
const { queryConfig } = req;
|
||||
const { pagination, sort, filters, paginationDisabled } = queryConfig;
|
||||
const { select, search, searchColumns, distinctColumns } = req.query as {
|
||||
select?: string;
|
||||
search?: string;
|
||||
searchColumns?: string;
|
||||
distinctColumns?: string;
|
||||
};
|
||||
|
||||
console.log(req.query);
|
||||
console.log(select);
|
||||
|
||||
// --- 🔍 Suche (im Backend mit Joins) ---
|
||||
if (search && search.trim().length > 0) {
|
||||
// 1. Alle Daten mit Joins holen (OHNE Pagination, aber mit Filtern)
|
||||
let searchQuery = server.supabase
|
||||
.from(resource)
|
||||
.select(select || dataTypes[resource].supabaseSelectWithInformation)
|
||||
.eq("tenant", req.user.tenant_id);
|
||||
|
||||
// --- Filterung anwenden ---
|
||||
for (const [key, val] of Object.entries(filters || {})) {
|
||||
if (Array.isArray(val)) {
|
||||
searchQuery = searchQuery.in(key, val);
|
||||
} else { // @ts-ignore
|
||||
if (val === true || val === false || val === null) {
|
||||
searchQuery = searchQuery.is(key, val);
|
||||
} else {
|
||||
searchQuery = searchQuery.eq(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { data: allData, error: searchError } = await searchQuery;
|
||||
|
||||
if (searchError) {
|
||||
server.log.error(searchError);
|
||||
return reply.code(400).send({ error: searchError.message });
|
||||
}
|
||||
|
||||
// 2. Im Backend nach Suchbegriff filtern
|
||||
const searchTerm = search.trim().toLowerCase();
|
||||
const searchCols = searchColumns
|
||||
? searchColumns.split(",").map(c => c.trim()).filter(Boolean)
|
||||
: dataTypes[resource].searchableColumns || [];
|
||||
|
||||
const filteredData = (allData || []).filter(row => {
|
||||
/*if (searchCols.length === 0) {
|
||||
// Fallback: Durchsuche alle String-Felder der Hauptebene
|
||||
return Object.values(row).some(val =>
|
||||
JSON.stringify(val).toString().toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
|
||||
return searchCols.some(col => {
|
||||
const value = getNestedValue(row, col);
|
||||
return JSON.stringify(value).toLowerCase().includes(searchTerm);
|
||||
});*/
|
||||
|
||||
return JSON.stringify(row).toLowerCase().includes(searchTerm);
|
||||
|
||||
});
|
||||
|
||||
// 3. Im Backend sortieren
|
||||
let sortedData = [...filteredData];
|
||||
if (sort?.length > 0) {
|
||||
sortedData.sort((a, b) => {
|
||||
for (const s of sort) {
|
||||
const aVal = getNestedValue(a, s.field);
|
||||
const bVal = getNestedValue(b, s.field);
|
||||
const comparison = compareValues(aVal, bVal);
|
||||
if (comparison !== 0) {
|
||||
return s.direction === "asc" ? comparison : -comparison;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Im Backend paginieren
|
||||
const total = sortedData.length;
|
||||
const paginatedData = !paginationDisabled && pagination
|
||||
? sortedData.slice(pagination.offset, pagination.offset + pagination.limit)
|
||||
: sortedData;
|
||||
|
||||
// 5. Distinct Values berechnen
|
||||
const distinctValues: Record<string, any[]> = {};
|
||||
if (distinctColumns) {
|
||||
const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean);
|
||||
|
||||
for (const col of cols) {
|
||||
// Distinct values aus den gefilterten Daten
|
||||
const values = filteredData
|
||||
.map(row => getNestedValue(row, col))
|
||||
.filter(v => v !== null && v !== undefined && v !== "");
|
||||
distinctValues[col] = [...new Set(values)].sort();
|
||||
}
|
||||
}
|
||||
|
||||
const totalPages = !paginationDisabled && pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
: 1;
|
||||
|
||||
const enrichedConfig = {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages,
|
||||
distinctValues,
|
||||
search: search || null,
|
||||
};
|
||||
|
||||
return { data: paginatedData, queryConfig: enrichedConfig };
|
||||
}
|
||||
|
||||
// --- Standardabfrage (ohne Suche) ---
|
||||
let baseQuery = server.supabase
|
||||
.from(resource)
|
||||
.select(select || dataTypes[resource].supabaseSelectWithInformation, { count: "exact" })
|
||||
.eq("tenant", req.user.tenant_id);
|
||||
|
||||
// --- Filterung ---
|
||||
for (const [key, val] of Object.entries(filters || {})) {
|
||||
if (Array.isArray(val)) {
|
||||
baseQuery = baseQuery.in(key, val);
|
||||
} else { // @ts-ignore
|
||||
if (val == true || val == false || val === null) {
|
||||
baseQuery = baseQuery.is(key, val);
|
||||
} else {
|
||||
baseQuery = baseQuery.eq(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sortierung ---
|
||||
if (sort?.length > 0) {
|
||||
for (const s of sort) {
|
||||
baseQuery = baseQuery.order(s.field, { ascending: s.direction === "asc" });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pagination ---
|
||||
if (!paginationDisabled && pagination) {
|
||||
const { offset, limit } = pagination;
|
||||
baseQuery = baseQuery.range(offset, offset + limit - 1);
|
||||
}
|
||||
|
||||
const { data, error, count } = await baseQuery;
|
||||
if (error) {
|
||||
server.log.error(error);
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
// --- Distinct-Werte (auch ohne Suche) ---
|
||||
const distinctValues: Record<string, any[]> = {};
|
||||
if (distinctColumns) {
|
||||
const cols = distinctColumns.split(",").map(c => c.trim()).filter(Boolean);
|
||||
|
||||
for (const col of cols) {
|
||||
const { data: allRows, error: distinctErr } = await server.supabase
|
||||
.from(resource)
|
||||
.select(col)
|
||||
.eq("tenant", req.user.tenant_id);
|
||||
|
||||
if (distinctErr) continue;
|
||||
|
||||
const values = (allRows || [])
|
||||
.map((row) => row?.[col] ?? null)
|
||||
.filter((v) => v !== null && v !== undefined && v !== "");
|
||||
distinctValues[col] = [...new Set(values)].sort();
|
||||
}
|
||||
}
|
||||
|
||||
const total = count || 0;
|
||||
const totalPages = !paginationDisabled && pagination?.limit
|
||||
? Math.ceil(total / pagination.limit)
|
||||
: 1;
|
||||
|
||||
const enrichedConfig = {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages,
|
||||
distinctValues,
|
||||
search: search || null,
|
||||
};
|
||||
|
||||
return { data, queryConfig: enrichedConfig };
|
||||
});
|
||||
|
||||
|
||||
// Detail
|
||||
server.get("/resource/:resource/:id/:with_information?", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({error: "No tenant selected"});
|
||||
}
|
||||
|
||||
const {resource, id, with_information} = req.params as {
|
||||
resource: string;
|
||||
id: string,
|
||||
with_information: boolean
|
||||
};
|
||||
const {select} = req.query as { select?: string }
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const {
|
||||
data,
|
||||
error
|
||||
} = await server.supabase.from(resource).select(with_information ? dataTypes[resource].supabaseSelectWithInformation : (select ? select : "*"))
|
||||
.eq("id", id)
|
||||
.eq("tenant", req.user.tenant_id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return reply.code(404).send({error: "Not found"});
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
// Create
|
||||
server.post("/resource/:resource", async (req, reply) => {
|
||||
if (!req.user?.tenant_id) {
|
||||
return reply.code(400).send({error: "No tenant selected"});
|
||||
}
|
||||
|
||||
const {resource} = req.params as { resource: string };
|
||||
const body = req.body as Record<string, any>;
|
||||
|
||||
const dataType = dataTypes[resource];
|
||||
let createData = {
|
||||
...body,
|
||||
tenant: req.user.tenant_id,
|
||||
archived: false, // Standardwert
|
||||
}
|
||||
|
||||
if (dataType.numberRangeHolder && !body[dataType.numberRangeHolder]) {
|
||||
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource)
|
||||
createData[dataType.numberRangeHolder] = result.usedNumber
|
||||
}
|
||||
|
||||
const {data, error} = await server.supabase
|
||||
.from(resource)
|
||||
.insert(createData)
|
||||
.select("*")
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return reply.code(400).send({error: error.message});
|
||||
}
|
||||
|
||||
await insertHistoryItem(server, {
|
||||
entity: resource,
|
||||
entityId: data.id,
|
||||
action: "created",
|
||||
created_by: req.user.user_id,
|
||||
tenant_id: req.user.tenant_id,
|
||||
oldVal: null,
|
||||
newVal: data,
|
||||
text: `${dataType.labelSingle} erstellt`,
|
||||
});
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
// UPDATE (inkl. Soft-Delete/Archive)
|
||||
server.put("/resource/:resource/:id", async (req, reply) => {
|
||||
console.log("hi")
|
||||
const {resource, id} = req.params as { resource: string; id: string }
|
||||
const body = req.body as Record<string, any>
|
||||
|
||||
const tenantId = (req.user as any)?.tenant_id
|
||||
const userId = (req.user as any)?.user_id
|
||||
|
||||
if (!tenantId || !userId) {
|
||||
return reply.code(401).send({error: "Unauthorized"})
|
||||
}
|
||||
|
||||
// vorherige Version für History laden
|
||||
const {data: oldItem} = await server.supabase
|
||||
.from(resource)
|
||||
.select("*")
|
||||
.eq("id", id)
|
||||
.eq("tenant", tenantId)
|
||||
.single()
|
||||
|
||||
const {data: newItem, error} = await server.supabase
|
||||
.from(resource)
|
||||
.update({...body, updated_at: new Date().toISOString(), updated_by: userId})
|
||||
.eq("id", id)
|
||||
.eq("tenant", tenantId)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) return reply.code(500).send({error})
|
||||
|
||||
const diffs = diffObjects(oldItem, newItem);
|
||||
|
||||
|
||||
for (const d of diffs) {
|
||||
await insertHistoryItem(server, {
|
||||
entity: resource,
|
||||
entityId: id,
|
||||
action: d.type,
|
||||
created_by: userId,
|
||||
tenant_id: tenantId,
|
||||
oldVal: d.oldValue ? String(d.oldValue) : null,
|
||||
newVal: d.newValue ? String(d.newValue) : null,
|
||||
text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""} → ${d.newValue ?? ""}`,
|
||||
});
|
||||
}
|
||||
|
||||
return newItem
|
||||
})
|
||||
}
|
||||
384
src/routes/resources/main.ts
Normal file
384
src/routes/resources/main.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import {
|
||||
eq,
|
||||
ilike,
|
||||
asc,
|
||||
desc,
|
||||
and,
|
||||
count,
|
||||
inArray,
|
||||
or
|
||||
} from "drizzle-orm"
|
||||
|
||||
|
||||
|
||||
import {resourceConfig} from "../../resource.config";
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// SQL Volltextsuche auf mehreren Feldern
|
||||
// -------------------------------------------------------------
|
||||
|
||||
|
||||
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 resourceRoutes(server: FastifyInstance) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// LIST
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/:resource", 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
|
||||
}
|
||||
|
||||
const {resource} = req.params as {resource: string}
|
||||
const table = resourceConfig[resource].table
|
||||
|
||||
// WHERE-Basis
|
||||
let whereCond: any = eq(table.tenant, tenantId)
|
||||
|
||||
// 🔍 SQL Search
|
||||
if(search) {
|
||||
const searchCond = buildSearchCondition(
|
||||
table,
|
||||
resourceConfig[resource].searchColumns,
|
||||
search.trim()
|
||||
)
|
||||
|
||||
if (searchCond) {
|
||||
whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
}
|
||||
|
||||
// Base Query
|
||||
let q = server.db.select().from(table).where(whereCond)
|
||||
|
||||
// Sortierung
|
||||
if (sort) {
|
||||
const col = (table as any)[sort]
|
||||
if (col) {
|
||||
//@ts-ignore
|
||||
q = ascQuery === "true"
|
||||
? q.orderBy(asc(col))
|
||||
: q.orderBy(desc(col))
|
||||
}
|
||||
}
|
||||
|
||||
const queryData = await q
|
||||
|
||||
|
||||
// RELATION LOADING (MANY-TO-ONE)
|
||||
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
let data = []
|
||||
|
||||
if(resourceConfig[resource].mtoLoad) {
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))];
|
||||
})
|
||||
|
||||
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : []
|
||||
|
||||
}
|
||||
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
|
||||
})
|
||||
|
||||
data = queryData.map(row => {
|
||||
let toReturn = {
|
||||
...row
|
||||
}
|
||||
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
||||
})
|
||||
|
||||
return toReturn
|
||||
});
|
||||
} else {
|
||||
data = queryData
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR /resource/:resource", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PAGINATED LIST
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/:resource/paginated", async (req, reply) => {
|
||||
try {
|
||||
const tenantId = req.user?.tenant_id;
|
||||
if (!tenantId) {
|
||||
return reply.code(400).send({ error: "No tenant selected" });
|
||||
}
|
||||
|
||||
const {resource} = req.params as {resource: string};
|
||||
|
||||
const {queryConfig} = req;
|
||||
const {
|
||||
pagination,
|
||||
sort,
|
||||
filters,
|
||||
paginationDisabled
|
||||
} = queryConfig;
|
||||
|
||||
const { search, distinctColumns } = req.query as {
|
||||
search?: string;
|
||||
distinctColumns?: string;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
let table = resourceConfig[resource].table
|
||||
|
||||
|
||||
let whereCond: any = eq(table.tenant, tenantId);
|
||||
|
||||
|
||||
if(search) {
|
||||
const searchCond = buildSearchCondition(
|
||||
table,
|
||||
resourceConfig[resource].searchColumns,
|
||||
search.trim()
|
||||
)
|
||||
|
||||
if (searchCond) {
|
||||
whereCond = and(whereCond, searchCond)
|
||||
}
|
||||
}
|
||||
|
||||
if (filters) {
|
||||
for (const [key, val] of Object.entries(filters)) {
|
||||
const col = (table 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// COUNT (for pagination)
|
||||
// -----------------------------------------------
|
||||
const totalRes = await server.db
|
||||
.select({ value: count(table.id) })
|
||||
.from(table)
|
||||
.where(whereCond);
|
||||
|
||||
const total = Number(totalRes[0]?.value ?? 0);
|
||||
|
||||
// -----------------------------------------------
|
||||
// DISTINCT VALUES (regardless of pagination)
|
||||
// -----------------------------------------------
|
||||
const distinctValues: Record<string, any[]> = {};
|
||||
|
||||
if (distinctColumns) {
|
||||
for (const colName of distinctColumns.split(",").map(c => c.trim())) {
|
||||
const col = (table as any)[colName];
|
||||
if (!col) continue;
|
||||
|
||||
const rows = await server.db
|
||||
.select({ v: col })
|
||||
.from(table)
|
||||
.where(eq(table.tenant, tenantId));
|
||||
|
||||
const values = rows
|
||||
.map(r => r.v)
|
||||
.filter(v => v != null && v !== "");
|
||||
|
||||
distinctValues[colName] = [...new Set(values)].sort();
|
||||
}
|
||||
}
|
||||
|
||||
// PAGINATION
|
||||
const offset = pagination?.offset ?? 0;
|
||||
const limit = pagination?.limit ?? 100;
|
||||
|
||||
// SORTING
|
||||
let orderField: any = null;
|
||||
let direction: "asc" | "desc" = "asc";
|
||||
|
||||
if (sort?.length > 0) {
|
||||
const s = sort[0];
|
||||
const col = (table as any)[s.field];
|
||||
if (col) {
|
||||
orderField = col;
|
||||
direction = s.direction === "asc" ? "asc" : "desc";
|
||||
}
|
||||
}
|
||||
|
||||
// MAIN QUERY (Paginated)
|
||||
let q = server.db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(whereCond)
|
||||
.offset(offset)
|
||||
.limit(limit);
|
||||
|
||||
if (orderField) {
|
||||
//@ts-ignore
|
||||
q = direction === "asc"
|
||||
? q.orderBy(asc(orderField))
|
||||
: q.orderBy(desc(orderField));
|
||||
}
|
||||
|
||||
const rows = await q;
|
||||
|
||||
if (!rows.length) {
|
||||
return {
|
||||
data: [],
|
||||
queryConfig: {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages: 0,
|
||||
distinctValues
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// RELATION LOADING (MANY-TO-ONE)
|
||||
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
let data = []
|
||||
|
||||
if(resourceConfig[resource].mtoLoad) {
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))];
|
||||
})
|
||||
|
||||
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||
lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : []
|
||||
|
||||
}
|
||||
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i]));
|
||||
})
|
||||
|
||||
data = rows.map(row => {
|
||||
let toReturn = {
|
||||
...row
|
||||
}
|
||||
|
||||
resourceConfig[resource].mtoLoad.forEach(relation => {
|
||||
toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null
|
||||
})
|
||||
|
||||
return toReturn
|
||||
});
|
||||
} else {
|
||||
data = rows
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// RETURN DATA
|
||||
// -----------------------------------------------
|
||||
return {
|
||||
data,
|
||||
queryConfig: {
|
||||
...queryConfig,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
distinctValues
|
||||
}
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
console.error(`ERROR /resource/:resource/paginated:`, err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// DETAIL (mit JOINS)
|
||||
// -------------------------------------------------------------
|
||||
server.get("/resource/:resource/: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 {resource} = req.params as { resource: string }
|
||||
const table = resourceConfig[resource].table
|
||||
|
||||
const projRows = await server.db
|
||||
.select()
|
||||
.from(table)
|
||||
.where(and(eq(table.id, id), eq(table.tenant, tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (!projRows.length)
|
||||
return reply.code(404).send({ error: "Resource not found" })
|
||||
|
||||
// ------------------------------------
|
||||
// LOAD RELATIONS
|
||||
// ------------------------------------
|
||||
|
||||
let ids = {}
|
||||
let lists = {}
|
||||
let maps = {}
|
||||
let data = {
|
||||
...projRows[0]
|
||||
}
|
||||
|
||||
if(resourceConfig[resource].mtoLoad) {
|
||||
for await (const relation of resourceConfig[resource].mtoLoad ) {
|
||||
if(data[relation]) {
|
||||
data[relation] = await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation]))
|
||||
}
|
||||
}
|
||||
|
||||
for await (const relation of resourceConfig[resource].mtmLoad ) {
|
||||
console.log(relation)
|
||||
data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
} catch (err) {
|
||||
console.error("ERROR /resource/projects/:id", err)
|
||||
return reply.code(500).send({ error: "Internal Server Error" })
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,16 +4,24 @@ import { StaffTimeEntry } from '../../types/staff'
|
||||
export default async function staffTimeRoutes(server: FastifyInstance) {
|
||||
|
||||
// ▶ Neue Zeit starten
|
||||
server.post<{ Body: Pick<StaffTimeEntry, 'started_at' | 'stopped_at' | 'type' | 'description'> }>(
|
||||
server.post(
|
||||
'/staff/time',
|
||||
async (req, reply) => {
|
||||
const { started_at, stopped_at, type = 'work', description } = req.body
|
||||
const { started_at, stopped_at, type = 'work', description, user_id } = req.body as any
|
||||
const userId = req.user.user_id
|
||||
const tenantId = req.user.tenant_id
|
||||
|
||||
|
||||
let dataToInsert = {
|
||||
tenant_id: tenantId,
|
||||
user_id: user_id ? user_id : userId,
|
||||
// @ts-ignore
|
||||
...req.body
|
||||
}
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('staff_time_entries')
|
||||
.insert([{ tenant_id: tenantId, user_id: userId, started_at, stopped_at, type, description }])
|
||||
.insert([dataToInsert])
|
||||
.select()
|
||||
.maybeSingle()
|
||||
|
||||
|
||||
27
src/utils/dbSearch.ts
Normal file
27
src/utils/dbSearch.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ilike, or } from "drizzle-orm"
|
||||
|
||||
/**
|
||||
* Erzeugt eine OR-Suchbedingung über mehrere Spalten
|
||||
*
|
||||
* @param table - Drizzle Table Schema
|
||||
* @param columns - Array der Spaltennamen (property names im schema)
|
||||
* @param search - Suchbegriff
|
||||
*/
|
||||
export function buildSearchWhere(table: any, columns: string[], search: string) {
|
||||
if (!search || !columns.length) return undefined
|
||||
|
||||
const term = `%${search.toLowerCase()}%`
|
||||
|
||||
const parts = columns
|
||||
.map((colName) => {
|
||||
const col = table[colName]
|
||||
if (!col) return null
|
||||
return ilike(col, term)
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (parts.length === 0) return undefined
|
||||
|
||||
// @ts-ignore
|
||||
return or(...parts)
|
||||
}
|
||||
114
src/utils/export/sepa.ts
Normal file
114
src/utils/export/sepa.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import xmlbuilder from "xmlbuilder";
|
||||
import {randomUUID} from "node:crypto";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export const createSEPAExport = async (server,idsToExport, tenant_id) => {
|
||||
const {data,error} = await server.supabase.from("createddocuments").select().eq("tenant", tenant_id).in("id", idsToExport)
|
||||
const {data:tenantData,error:tenantError} = await server.supabase.from("tenants").select().eq("id", tenant_id).single()
|
||||
console.log(tenantData)
|
||||
console.log(tenantError)
|
||||
|
||||
console.log(data)
|
||||
|
||||
let transactions = []
|
||||
|
||||
let obj = {
|
||||
Document: {
|
||||
'@xmlns':"urn:iso:std:iso:20022:tech:xsd:pain.008.001.02",
|
||||
'CstmrDrctDbtInitn': {
|
||||
'GrpHdr': {
|
||||
'MsgId': randomUUID(),
|
||||
'CredDtTm': dayjs().format("YYYY-MM-DDTHH:mm:ss"),
|
||||
'NbOfTxs': transactions.length,
|
||||
'CtrlSum': 0, // TODO: Total Sum
|
||||
'InitgPty': {
|
||||
'Nm': tenantData.name
|
||||
}
|
||||
},
|
||||
'PmtInf': {
|
||||
'PmtInfId': "", // TODO: Mandatsreferenz,
|
||||
'PmtMtd': "DD",
|
||||
'BtchBookg': "true", // TODO: BatchBooking,
|
||||
'NbOfTxs': transactions.length,
|
||||
'CtrlSum': 0, //TODO: Total Sum
|
||||
'PmtTpInf': {
|
||||
'SvcLvl': {
|
||||
'Cd': "SEPA"
|
||||
},
|
||||
'LclInstrm': {
|
||||
'Cd': "CORE" // Core für BASIS / B2B für Firmen
|
||||
},
|
||||
'SeqTp': "RCUR" // TODO: Unterscheidung Einmalig / Wiederkehrend
|
||||
},
|
||||
'ReqdColltnDt': dayjs().add(3, "days").format("YYYY-MM-DDTHH:mm:ss"),
|
||||
'Cdtr': {
|
||||
'Nm': tenantData.name
|
||||
},
|
||||
'CdtrAcct': {
|
||||
'Id': {
|
||||
'IBAN': "DE" // TODO: IBAN Gläubiger EINSETZEN
|
||||
}
|
||||
},
|
||||
'CdtrAgt': {
|
||||
'FinInstnId': {
|
||||
'BIC': "BIC" // TODO: BIC des Gläubigers einsetzen
|
||||
}
|
||||
},
|
||||
'ChrgBr': "SLEV",
|
||||
'CdtrSchmeId': {
|
||||
'Id': {
|
||||
'PrvtId': {
|
||||
'Othr': {
|
||||
'Id': tenantData.creditorId,
|
||||
'SchmeNm': {
|
||||
'Prty': "SEPA"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
//TODO ITERATE ALL INVOICES HERE
|
||||
'DrctDbtTxInf': {
|
||||
'PmtId': {
|
||||
'EndToEndId': "" // TODO: EINDEUTIGE ReFERENZ wahrscheinlich RE Nummer
|
||||
},
|
||||
'InstdAmt': {
|
||||
'@Ccy':"EUR",
|
||||
'#text':100 //TODO: Rechnungssumme zwei NK mit Punkt
|
||||
},
|
||||
'DrctDbtTx': {
|
||||
'MndtRltdInf': {
|
||||
'MndtId': "", // TODO: Mandatsref,
|
||||
'DtOfSgntr': "", //TODO: Unterschrieben am,
|
||||
'AmdmntInd': "" //TODO: Mandat geändert
|
||||
}
|
||||
},
|
||||
'DbtrAgt': {
|
||||
'FinInstnId': {
|
||||
'BIC': "", //TODO: BIC Debtor
|
||||
}
|
||||
},
|
||||
'Dbtr': {
|
||||
'Nm': "" // TODO NAME Debtor
|
||||
},
|
||||
'DbtrAcct': {
|
||||
'Id': {
|
||||
'IBAN': "DE" // TODO IBAN Debtor
|
||||
}
|
||||
},
|
||||
'RmtInf': {
|
||||
'Ustrd': "" //TODO Verwendungszweck Rechnungsnummer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
|
||||
|
||||
console.log(doc.end({pretty:true}))
|
||||
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import {FastifyInstance} from "fastify";
|
||||
// import { PNG } from 'pngjs'
|
||||
// import { ready as zplReady } from 'zpl-renderer-js'
|
||||
// import { Utils } from '@mmote/niimbluelib'
|
||||
// import { createCanvas } from 'canvas'
|
||||
// import bwipjs from 'bwip-js'
|
||||
// import Sharp from 'sharp'
|
||||
// import fs from 'fs'
|
||||
|
||||
export const useNextNumberRangeNumber = async (server:FastifyInstance, tenantId:number,numberRange)=> {
|
||||
const {data:tenant} = await server.supabase.from("tenants").select().eq("id",tenantId).single()
|
||||
@@ -20,4 +27,118 @@ export const useNextNumberRangeNumber = async (server:FastifyInstance, tenantId:
|
||||
usedNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
|
||||
// 1️⃣ PNG dekodieren
|
||||
const buffer = Buffer.from(base64Png, 'base64')
|
||||
const png = PNG.sync.read(buffer) // liefert {width, height, data: Uint8Array(RGBA)}
|
||||
|
||||
const { width, height, data } = png
|
||||
console.log(width, height, data)
|
||||
const cols = printDirection === 'left' ? height : width
|
||||
const rows = printDirection === 'left' ? width : height
|
||||
const rowsData = []
|
||||
|
||||
console.log(cols)
|
||||
|
||||
if (cols % 8 !== 0) throw new Error('Column count must be multiple of 8')
|
||||
|
||||
// 2️⃣ Zeilenweise durchgehen und Bits bilden
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let isVoid = true
|
||||
let blackPixelsCount = 0
|
||||
const rowData = new Uint8Array(cols / 8)
|
||||
|
||||
for (let colOct = 0; colOct < cols / 8; colOct++) {
|
||||
let pixelsOctet = 0
|
||||
for (let colBit = 0; colBit < 8; colBit++) {
|
||||
const x = printDirection === 'left' ? row : colOct * 8 + colBit
|
||||
const y = printDirection === 'left' ? height - 1 - (colOct * 8 + colBit) : row
|
||||
const idx = (y * width + x) * 4
|
||||
const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]
|
||||
const isBlack = lum < 128
|
||||
if (isBlack) {
|
||||
pixelsOctet |= 1 << (7 - colBit)
|
||||
isVoid = false
|
||||
blackPixelsCount++
|
||||
}
|
||||
}
|
||||
rowData[colOct] = pixelsOctet
|
||||
}
|
||||
|
||||
const newPart = {
|
||||
dataType: isVoid ? 'void' : 'pixels',
|
||||
rowNumber: row,
|
||||
repeat: 1,
|
||||
rowData: isVoid ? undefined : rowData,
|
||||
blackPixelsCount,
|
||||
}
|
||||
|
||||
if (rowsData.length === 0) {
|
||||
rowsData.push(newPart)
|
||||
} else {
|
||||
const last = rowsData[rowsData.length - 1]
|
||||
let same = newPart.dataType === last.dataType
|
||||
if (same && newPart.dataType === 'pixels') {
|
||||
same = Utils.u8ArraysEqual(newPart.rowData, last.rowData)
|
||||
}
|
||||
if (same) last.repeat++
|
||||
else rowsData.push(newPart)
|
||||
if (row % 200 === 199) {
|
||||
rowsData.push({
|
||||
dataType: 'check',
|
||||
rowNumber: row,
|
||||
repeat: 0,
|
||||
rowData: undefined,
|
||||
blackPixelsCount: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { cols, rows, rowsData }
|
||||
}
|
||||
|
||||
export async function generateLabel(context,width,height) {
|
||||
// Canvas für Hintergrund & Text
|
||||
const canvas = createCanvas(width, height)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Hintergrund weiß
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Überschrift
|
||||
ctx.fillStyle = '#000000'
|
||||
ctx.font = '32px Arial'
|
||||
ctx.fillText(context.text, 20, 40)
|
||||
|
||||
// 3) DataMatrix
|
||||
const dataMatrixPng = await bwipjs.toBuffer({
|
||||
bcid: 'datamatrix',
|
||||
text: context.datamatrix,
|
||||
scale: 6,
|
||||
})
|
||||
|
||||
// Basisbild aus Canvas
|
||||
const base = await Sharp(canvas.toBuffer())
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
// Alles zusammen compositen
|
||||
const final = await Sharp(base)
|
||||
.composite([
|
||||
{ input: dataMatrixPng, top: 60, left: 20 },
|
||||
])
|
||||
.png()
|
||||
.toBuffer()
|
||||
|
||||
fs.writeFileSync('label.png', final)
|
||||
|
||||
// Optional: Base64 zurückgeben (z.B. für API)
|
||||
const base64 = final.toString('base64')
|
||||
|
||||
return base64
|
||||
}*/
|
||||
|
||||
@@ -13,6 +13,7 @@ export let secrets = {
|
||||
JWT_SECRET: string
|
||||
PORT: number
|
||||
HOST: string
|
||||
DATABASE_URL: string
|
||||
SUPABASE_URL: string
|
||||
SUPABASE_SERVICE_ROLE_KEY: string
|
||||
S3_BUCKET: string
|
||||
|
||||
Reference in New Issue
Block a user