Merge Backend in FEDEO MONO
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
|
||||
/src/generated/prisma
|
||||
18
.gitlab-ci.yml
Normal file
18
.gitlab-ci.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
before_script:
|
||||
- docker info
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
build-backend:
|
||||
stage: build
|
||||
tags:
|
||||
- shell
|
||||
- docker-daemon
|
||||
variables:
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
|
||||
script:
|
||||
- echo $IMAGE_TAG
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker build -t $IMAGE_TAG .
|
||||
- docker push $IMAGE_TAG
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Package-Dateien
|
||||
COPY package*.json ./
|
||||
|
||||
# Dev + Prod Dependencies (für TS-Build nötig)
|
||||
RUN npm install
|
||||
|
||||
# Restlicher Sourcecode
|
||||
COPY . .
|
||||
|
||||
# TypeScript Build
|
||||
RUN npm run build
|
||||
|
||||
# Port freigeben
|
||||
EXPOSE 3100
|
||||
|
||||
# Start der App
|
||||
CMD ["node", "dist/src/index.js"]
|
||||
32
TODO.md
Normal file
32
TODO.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Projekt To-Do Liste
|
||||
|
||||
## ✅ Erledigt
|
||||
- JWT-basierte Authentifizierung mit Cookie
|
||||
- Prefix für Auth-Tabellen (`auth_users`, `auth_roles`, …)
|
||||
- `/me` liefert User + Rechte (via `auth_get_user_permissions`)
|
||||
- Basis-Seed für Standardrollen + Rechte eingespielt
|
||||
- Auth Middleware im Frontend korrigiert (Login-Redirects)
|
||||
- Nuxt API Plugin unterstützt JWT im Header
|
||||
- Login-Seite an Nuxt UI Pro (v2) anpassen
|
||||
- `usePermission()` Composable im Frontend vorbereitet
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Offene Punkte
|
||||
|
||||
### Backend
|
||||
- [ ] `/me` erweitern: Rollen + deren Rechte strukturiert zurückgeben (`{ role: "manager", permissions: [...] }`)
|
||||
- [ ] Loading Flag im Auth-Flow berücksichtigen (damit `me` nicht doppelt feuert)
|
||||
- [ ] Gemeinsames Schema für Entities (Backend stellt per Endpoint bereit)
|
||||
- [ ] Soft Delete vereinheitlichen (`archived = true` statt DELETE)
|
||||
- [ ] Swagger-Doku verbessern (Schemas, Beispiele)
|
||||
|
||||
### Frontend
|
||||
- [ ] Loading Flag in Middleware/Store einbauen
|
||||
- [ ] Einheitliches Laden des Schemas beim Start
|
||||
- [ ] Pinia-Store für Auth/User/Tenant konsolidieren
|
||||
- [ ] Composable `usePermission(key)` implementieren, um Rechte einfach im Template zu prüfen
|
||||
- [ ] Entity-Seiten schrittweise auf API-Routen umstellen
|
||||
- [ ] Page Guards für Routen einbauen (z. B. `/projects/create` nur bei `projects-create`)
|
||||
|
||||
---
|
||||
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)
|
||||
1312
db/migrations/0000_brief_dark_beast.sql
Normal file
1312
db/migrations/0000_brief_dark_beast.sql
Normal file
File diff suppressed because it is too large
Load Diff
32
db/migrations/0001_medical_big_bertha.sql
Normal file
32
db/migrations/0001_medical_big_bertha.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
CREATE TABLE "time_events" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"actor_type" text NOT NULL,
|
||||
"actor_user_id" uuid,
|
||||
"event_time" timestamp with time zone NOT NULL,
|
||||
"event_type" text NOT NULL,
|
||||
"source" text NOT NULL,
|
||||
"invalidates_event_id" uuid,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "time_events_actor_user_check" CHECK (
|
||||
(actor_type = 'system' AND actor_user_id IS NULL)
|
||||
OR
|
||||
(actor_type = 'user' AND actor_user_id IS NOT NULL)
|
||||
)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "bankstatements" DROP CONSTRAINT "bankstatements_incomingInvoice_incominginvoices_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "bankstatements" DROP CONSTRAINT "bankstatements_createdDocument_createddocuments_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_actor_user_id_auth_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "time_events" ADD CONSTRAINT "time_events_invalidates_event_id_time_events_id_fk" FOREIGN KEY ("invalidates_event_id") REFERENCES "public"."time_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "idx_time_events_tenant_user_time" ON "time_events" USING btree ("tenant_id","user_id","event_time");--> statement-breakpoint
|
||||
CREATE INDEX "idx_time_events_created_at" ON "time_events" USING btree ("created_at");--> statement-breakpoint
|
||||
CREATE INDEX "idx_time_events_invalidates" ON "time_events" USING btree ("invalidates_event_id");--> statement-breakpoint
|
||||
ALTER TABLE "bankstatements" DROP COLUMN "incomingInvoice";--> statement-breakpoint
|
||||
ALTER TABLE "bankstatements" DROP COLUMN "createdDocument";
|
||||
13
db/migrations/0002_silent_christian_walker.sql
Normal file
13
db/migrations/0002_silent_christian_walker.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE "time_events" RENAME TO "staff_time_events";--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_tenant_id_tenants_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_user_id_auth_users_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_actor_user_id_auth_users_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" DROP CONSTRAINT "time_events_invalidates_event_id_time_events_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_actor_user_id_auth_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_invalidates_event_id_staff_time_events_id_fk" FOREIGN KEY ("invalidates_event_id") REFERENCES "public"."staff_time_events"("id") ON DELETE no action ON UPDATE no action;
|
||||
9788
db/migrations/meta/0000_snapshot.json
Normal file
9788
db/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
9947
db/migrations/meta/0001_snapshot.json
Normal file
9947
db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
41
db/migrations/meta/_journal.json
Normal file
41
db/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1764947303113,
|
||||
"tag": "0000_brief_dark_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1765641431341,
|
||||
"tag": "0001_medical_big_bertha",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1765642446738,
|
||||
"tag": "0002_silent_christian_walker",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1765716484200,
|
||||
"tag": "0003_woozy_adam_destine",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1765716877146,
|
||||
"tag": "0004_stormy_onslaught",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
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
|
||||
62
db/schema/bankstatements.ts
Normal file
62
db/schema/bankstatements.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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"),
|
||||
|
||||
mandateId: text("mandateId"),
|
||||
|
||||
contract: bigint("contract", { mode: "number" }).references(
|
||||
() => contracts.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
|
||||
124
db/schema/createddocuments.ts
Normal file
124
db/schema/createddocuments.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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"
|
||||
import {serialExecutions} from "./serialexecutions";
|
||||
|
||||
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({}),
|
||||
|
||||
createddocument: 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
|
||||
),
|
||||
|
||||
serialexecution: uuid("serialexecution").references(() => serialExecutions.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(),
|
||||
|
||||
state_code: 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
|
||||
74
db/schema/index.ts
Normal file
74
db/schema/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
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"
|
||||
export * from "./staff_time_events"
|
||||
export * from "./serialtypes"
|
||||
export * from "./serialexecutions"
|
||||
export * from "./public_links"
|
||||
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
|
||||
30
db/schema/public_links.ts
Normal file
30
db/schema/public_links.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { pgTable, text, integer, boolean, jsonb, timestamp, uuid } from 'drizzle-orm/pg-core';
|
||||
import { tenants } from './tenants';
|
||||
import { authProfiles } from './auth_profiles';
|
||||
|
||||
export const publicLinks = pgTable('public_links', {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
// Der öffentliche Token (z.B. "werkstatt-tablet-01")
|
||||
token: text('token').notNull().unique(),
|
||||
|
||||
// Zuordnung zum Tenant (WICHTIG für die Datentrennung)
|
||||
tenant: integer('tenant').references(() => tenants.id).notNull(),
|
||||
|
||||
defaultProfile: uuid('default_profile').references(() => authProfiles.id),
|
||||
|
||||
// Sicherheit
|
||||
isProtected: boolean('is_protected').default(false).notNull(),
|
||||
pinHash: text('pin_hash'),
|
||||
|
||||
// Konfiguration (JSON)
|
||||
config: jsonb('config').default({}),
|
||||
|
||||
// Metadaten
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
|
||||
active: boolean('active').default(true).notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow(),
|
||||
updatedAt: timestamp('updated_at').defaultNow(),
|
||||
});
|
||||
21
db/schema/serialexecutions.ts
Normal file
21
db/schema/serialexecutions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
import {tenants} from "./tenants";
|
||||
|
||||
export const serialExecutions = pgTable("serial_executions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id), executionDate: timestamp("execution_date").notNull(),
|
||||
status: text("status").default("draft"), // 'draft', 'completed'
|
||||
createdBy: text("created_by"), // oder UUID, je nach Auth-System
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
summary: text("summary"), // z.B. "25 Rechnungen erstellt"
|
||||
});
|
||||
40
db/schema/serialtypes.ts
Normal file
40
db/schema/serialtypes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
timestamp,
|
||||
text,
|
||||
jsonb,
|
||||
boolean,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const serialtypes = pgTable("serialtypes", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
name: text("name").notNull(),
|
||||
|
||||
intervall: text("intervall"),
|
||||
|
||||
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 SerialType = typeof serialtypes.$inferSelect
|
||||
export type NewSerialType = typeof serialtypes.$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(),
|
||||
|
||||
tenant_id: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
user_id: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id, { onDelete: "cascade" }),
|
||||
|
||||
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stopped_at: timestamp("stopped_at", { withTimezone: true }),
|
||||
|
||||
duration_minutes: 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"),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updated_by: uuid("updated_by").references(() => authUsers.id),
|
||||
|
||||
source: text("source"),
|
||||
|
||||
state: timesStateEnum("state").notNull().default("draft"),
|
||||
|
||||
device: uuid("device"),
|
||||
|
||||
internal_note: text("internal_note"),
|
||||
|
||||
vacation_reason: text("vacation_reason"),
|
||||
vacation_days: numeric("vacation_days", { precision: 5, scale: 2 }),
|
||||
|
||||
approved_by: uuid("approved_by").references(() => authUsers.id),
|
||||
approved_at: timestamp("approved_at", { withTimezone: true }),
|
||||
|
||||
sick_reason: 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 stafftimenetryconnects = pgTable("staff_time_entry_connects", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
stafftimeentry: uuid("time_entry_id")
|
||||
.notNull()
|
||||
.references(() => stafftimeentries.id, { onDelete: "cascade" }),
|
||||
|
||||
project_id: bigint("project_id", { mode: "number" }), // referenziert später projects.id
|
||||
|
||||
started_at: timestamp("started_at", { withTimezone: true }).notNull(),
|
||||
stopped_at: timestamp("stopped_at", { withTimezone: true }).notNull(),
|
||||
|
||||
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
|
||||
sql`(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)`
|
||||
),
|
||||
|
||||
notes: text("notes"),
|
||||
|
||||
created_at: timestamp("created_at", { withTimezone: true }).defaultNow(),
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow(),
|
||||
})
|
||||
|
||||
export type StaffTimeEntryConnect =
|
||||
typeof stafftimenetryconnects.$inferSelect
|
||||
export type NewStaffTimeEntryConnect =
|
||||
typeof stafftimenetryconnects.$inferInsert
|
||||
85
db/schema/staff_time_events.ts
Normal file
85
db/schema/staff_time_events.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
pgTable,
|
||||
uuid,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
jsonb,
|
||||
index,
|
||||
check,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import {tenants} from "./tenants";
|
||||
import {authUsers} from "./auth_users";
|
||||
|
||||
export const stafftimeevents = pgTable(
|
||||
"staff_time_events",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
tenant_id: bigint("tenant_id", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
user_id: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => authUsers.id),
|
||||
|
||||
// Akteur
|
||||
actortype: text("actor_type").notNull(), // 'user' | 'system'
|
||||
actoruser_id: uuid("actor_user_id").references(() => authUsers.id),
|
||||
|
||||
// Zeit
|
||||
eventtime: timestamp("event_time", {
|
||||
withTimezone: true,
|
||||
}).notNull(),
|
||||
|
||||
// Fachliche Bedeutung
|
||||
eventtype: text("event_type").notNull(),
|
||||
|
||||
// Quelle
|
||||
source: text("source").notNull(), // web | mobile | terminal | system
|
||||
|
||||
// Entkräftung
|
||||
invalidates_event_id: uuid("invalidates_event_id")
|
||||
.references(() => stafftimeevents.id),
|
||||
|
||||
//Beziehung Approval etc
|
||||
related_event_id: uuid("related_event_id")
|
||||
.references(() => stafftimeevents.id),
|
||||
|
||||
// Zusatzdaten
|
||||
metadata: jsonb("metadata"),
|
||||
|
||||
// Technisch
|
||||
created_at: timestamp("created_at", {
|
||||
withTimezone: true,
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
// Indizes
|
||||
tenantUserTimeIdx: index("idx_time_events_tenant_user_time").on(
|
||||
table.tenant_id,
|
||||
table.user_id,
|
||||
table.eventtime
|
||||
),
|
||||
|
||||
createdAtIdx: index("idx_time_events_created_at").on(table.created_at),
|
||||
|
||||
invalidatesIdx: index("idx_time_events_invalidates").on(
|
||||
table.invalidates_event_id
|
||||
),
|
||||
|
||||
// Constraints
|
||||
actorUserCheck: check(
|
||||
"time_events_actor_user_check",
|
||||
sql`
|
||||
(actor_type = 'system' AND actor_user_id IS NULL)
|
||||
OR
|
||||
(actor_type = 'user' AND actor_user_id IS NOT NULL)
|
||||
`
|
||||
),
|
||||
})
|
||||
);
|
||||
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
|
||||
bankstatement: integer("bs_id")
|
||||
.notNull()
|
||||
.references(() => bankstatements.id),
|
||||
|
||||
createddocument: integer("cd_id").references(() => createddocuments.id),
|
||||
|
||||
amount: doublePrecision("amount").notNull().default(0),
|
||||
|
||||
incominginvoice: 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
|
||||
),
|
||||
|
||||
created_at: 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),
|
||||
|
||||
updated_at: timestamp("updated_at", { withTimezone: true }),
|
||||
|
||||
updated_by: 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
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
backend:
|
||||
image: reg.federspiel.software/fedeo/backend:main
|
||||
restart: always
|
||||
|
||||
environment:
|
||||
|
||||
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,
|
||||
},
|
||||
})
|
||||
64
package.json
Normal file
64
package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/src/index.js",
|
||||
"schema:index": "ts-node scripts/generate-schema-index.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.federspiel.software/fedeo/backend.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.879.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.879.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@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",
|
||||
"handlebars": "^4.7.8",
|
||||
"imapflow": "^1.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mailparser": "^3.9.0",
|
||||
"nodemailer": "^7.0.6",
|
||||
"openai": "^6.10.0",
|
||||
"pdf-lib": "^1.17.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")
|
||||
166
src/index.ts
Normal file
166
src/index.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import Fastify from "fastify";
|
||||
import swaggerPlugin from "./plugins/swagger"
|
||||
import supabasePlugin from "./plugins/supabase";
|
||||
import dayjsPlugin from "./plugins/dayjs";
|
||||
import healthRoutes from "./routes/health";
|
||||
import meRoutes from "./routes/auth/me";
|
||||
import tenantRoutes from "./routes/tenant";
|
||||
import tenantPlugin from "./plugins/tenant";
|
||||
import authRoutes from "./routes/auth/auth";
|
||||
import authRoutesAuthenticated from "./routes/auth/auth-authenticated";
|
||||
import authPlugin from "./plugins/auth";
|
||||
import adminRoutes from "./routes/admin";
|
||||
import corsPlugin from "./plugins/cors";
|
||||
import queryConfigPlugin from "./plugins/queryconfig";
|
||||
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 functionRoutes from "./routes/functions";
|
||||
import bankingRoutes from "./routes/banking";
|
||||
import exportRoutes from "./routes/exports"
|
||||
import emailAsUserRoutes from "./routes/emailAsUser";
|
||||
import authProfilesRoutes from "./routes/profiles";
|
||||
import helpdeskRoutes from "./routes/helpdesk";
|
||||
import helpdeskInboundRoutes from "./routes/helpdesk.inbound";
|
||||
import notificationsRoutes from "./routes/notifications";
|
||||
import staffTimeRoutes from "./routes/staff/time";
|
||||
import staffTimeConnectRoutes from "./routes/staff/timeconnects";
|
||||
import userRoutes from "./routes/auth/user";
|
||||
import publiclinksAuthenticatedRoutes from "./routes/publiclinks/publiclinks-authenticated";
|
||||
|
||||
//Public Links
|
||||
import publiclinksNonAuthenticatedRoutes from "./routes/publiclinks/publiclinks-non-authenticated";
|
||||
|
||||
//Resources
|
||||
import resourceRoutes from "./routes/resources/main";
|
||||
|
||||
//M2M
|
||||
import authM2m from "./plugins/auth.m2m";
|
||||
import helpdeskInboundEmailRoutes from "./routes/helpdesk.inbound.email";
|
||||
import deviceRoutes from "./routes/internal/devices";
|
||||
import tenantRoutesInternal from "./routes/internal/tenant";
|
||||
import staffTimeRoutesInternal from "./routes/internal/time";
|
||||
|
||||
//Devices
|
||||
import devicesRFIDRoutes from "./routes/devices/rfid";
|
||||
|
||||
|
||||
import {sendMail} from "./utils/mailer";
|
||||
import {loadSecrets, secrets} from "./utils/secrets";
|
||||
import {initMailer} from "./utils/mailer"
|
||||
import {initS3} from "./utils/s3";
|
||||
|
||||
//Services
|
||||
import servicesPlugin from "./plugins/services";
|
||||
|
||||
async function main() {
|
||||
const app = Fastify({ logger: false });
|
||||
await loadSecrets();
|
||||
await initMailer();
|
||||
await initS3();
|
||||
|
||||
|
||||
|
||||
/*app.addHook("onRequest", (req, reply, done) => {
|
||||
console.log("Incoming:", req.method, req.url, "Headers:", req.headers)
|
||||
done()
|
||||
})*/
|
||||
|
||||
// Plugins Global verfügbar
|
||||
await app.register(swaggerPlugin);
|
||||
await app.register(corsPlugin);
|
||||
await app.register(supabasePlugin);
|
||||
await app.register(tenantPlugin);
|
||||
await app.register(dayjsPlugin);
|
||||
await app.register(dbPlugin);
|
||||
await app.register(servicesPlugin);
|
||||
|
||||
app.addHook('preHandler', (req, reply, done) => {
|
||||
console.log(req.method)
|
||||
console.log('Matched path:', req.routeOptions.url)
|
||||
console.log('Exact URL:', req.url)
|
||||
done()
|
||||
})
|
||||
|
||||
app.get('/health', async (req, res) => {
|
||||
return res.send({ status: 'ok' })
|
||||
})
|
||||
|
||||
//Plugin nur auf bestimmten Routes
|
||||
await app.register(queryConfigPlugin, {
|
||||
routes: ['/api/resource/:resource/paginated']
|
||||
})
|
||||
|
||||
app.register(fastifyCookie, {
|
||||
secret: secrets.COOKIE_SECRET,
|
||||
})
|
||||
// Öffentliche Routes
|
||||
await app.register(authRoutes);
|
||||
await app.register(healthRoutes);
|
||||
|
||||
await app.register(helpdeskInboundRoutes);
|
||||
|
||||
await app.register(publiclinksNonAuthenticatedRoutes)
|
||||
|
||||
|
||||
await app.register(async (m2mApp) => {
|
||||
await m2mApp.register(authM2m)
|
||||
await m2mApp.register(helpdeskInboundEmailRoutes)
|
||||
await m2mApp.register(deviceRoutes)
|
||||
await m2mApp.register(tenantRoutesInternal)
|
||||
await m2mApp.register(staffTimeRoutesInternal)
|
||||
},{prefix: "/internal"})
|
||||
|
||||
await app.register(async (devicesApp) => {
|
||||
await devicesApp.register(devicesRFIDRoutes)
|
||||
},{prefix: "/devices"})
|
||||
|
||||
|
||||
//Geschützte Routes
|
||||
|
||||
await app.register(async (subApp) => {
|
||||
await subApp.register(authPlugin);
|
||||
await subApp.register(authRoutesAuthenticated);
|
||||
await subApp.register(meRoutes);
|
||||
await subApp.register(tenantRoutes);
|
||||
await subApp.register(adminRoutes);
|
||||
await subApp.register(resourceRoutesSpecial);
|
||||
await subApp.register(historyRoutes);
|
||||
await subApp.register(fileRoutes);
|
||||
await subApp.register(functionRoutes);
|
||||
await subApp.register(bankingRoutes);
|
||||
await subApp.register(exportRoutes);
|
||||
await subApp.register(emailAsUserRoutes);
|
||||
await subApp.register(authProfilesRoutes);
|
||||
await subApp.register(helpdeskRoutes);
|
||||
await subApp.register(notificationsRoutes);
|
||||
await subApp.register(staffTimeRoutes);
|
||||
await subApp.register(staffTimeConnectRoutes);
|
||||
await subApp.register(userRoutes);
|
||||
await subApp.register(publiclinksAuthenticatedRoutes);
|
||||
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 {
|
||||
await app.listen({ port: secrets.PORT, host: secrets.HOST });
|
||||
console.log(`🚀 Server läuft auf http://${secrets.HOST}:${secrets.PORT}`);
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
253
src/modules/cron/bankstatementsync.service.ts
Normal file
253
src/modules/cron/bankstatementsync.service.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
// /services/bankStatementService.ts
|
||||
import axios from "axios"
|
||||
import dayjs from "dayjs"
|
||||
import utc from "dayjs/plugin/utc.js"
|
||||
import {secrets} from "../../utils/secrets"
|
||||
import {FastifyInstance} from "fastify"
|
||||
|
||||
// Drizzle imports
|
||||
import {
|
||||
bankaccounts,
|
||||
bankstatements,
|
||||
} from "../../../db/schema"
|
||||
|
||||
import {
|
||||
eq,
|
||||
and,
|
||||
isNull,
|
||||
} from "drizzle-orm"
|
||||
|
||||
dayjs.extend(utc)
|
||||
|
||||
interface BalanceAmount {
|
||||
amount: string
|
||||
currency: string
|
||||
}
|
||||
|
||||
interface BookedTransaction {
|
||||
bookingDate: string
|
||||
valueDate: string
|
||||
internalTransactionId: string
|
||||
transactionAmount: { amount: string; currency: string }
|
||||
|
||||
creditorAccount?: { iban?: string }
|
||||
creditorName?: string
|
||||
|
||||
debtorAccount?: { iban?: string }
|
||||
debtorName?: string
|
||||
|
||||
remittanceInformationUnstructured?: string
|
||||
remittanceInformationStructured?: string
|
||||
remittanceInformationStructuredArray?: string[]
|
||||
additionalInformation?: string
|
||||
}
|
||||
|
||||
interface TransactionsResponse {
|
||||
transactions: {
|
||||
booked: BookedTransaction[]
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
export function bankStatementService(server: FastifyInstance) {
|
||||
|
||||
let accessToken: string | null = null
|
||||
|
||||
// -----------------------------------------------
|
||||
// ✔ TOKEN LADEN
|
||||
// -----------------------------------------------
|
||||
const getToken = async () => {
|
||||
console.log("Fetching GoCardless token…")
|
||||
|
||||
const response = await axios.post(
|
||||
`${secrets.GOCARDLESS_BASE_URL}/token/new/`,
|
||||
{
|
||||
secret_id: secrets.GOCARDLESS_SECRET_ID,
|
||||
secret_key: secrets.GOCARDLESS_SECRET_KEY,
|
||||
}
|
||||
)
|
||||
|
||||
accessToken = response.data.access
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// ✔ Salden laden
|
||||
// -----------------------------------------------
|
||||
const getBalanceData = async (accountId: string): Promise<any | false> => {
|
||||
try {
|
||||
const {data} = await axios.get(
|
||||
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/balances`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return data
|
||||
} catch (err: any) {
|
||||
server.log.error(err.response?.data ?? err)
|
||||
|
||||
const expired =
|
||||
err.response?.data?.summary?.includes("expired") ||
|
||||
err.response?.data?.detail?.includes("expired")
|
||||
|
||||
if (expired) {
|
||||
await server.db
|
||||
.update(bankaccounts)
|
||||
.set({expired: true})
|
||||
.where(eq(bankaccounts.accountId, accountId))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// ✔ Transaktionen laden
|
||||
// -----------------------------------------------
|
||||
const getTransactionData = async (accountId: string) => {
|
||||
try {
|
||||
const {data} = await axios.get<TransactionsResponse>(
|
||||
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/transactions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return data.transactions.booked
|
||||
} catch (err: any) {
|
||||
server.log.error(err.response?.data ?? err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
// ✔ Haupt-Sync-Prozess
|
||||
// -----------------------------------------------
|
||||
const syncAccounts = async (tenantId:number) => {
|
||||
try {
|
||||
console.log("Starting account sync…")
|
||||
|
||||
// 🟦 DB: Aktive Accounts
|
||||
const accounts = await server.db
|
||||
.select()
|
||||
.from(bankaccounts)
|
||||
.where(and(eq(bankaccounts.expired, false),eq(bankaccounts.tenant, tenantId)))
|
||||
|
||||
if (!accounts.length) return
|
||||
|
||||
const allNewTransactions: any[] = []
|
||||
|
||||
for (const account of accounts) {
|
||||
|
||||
// ---------------------------
|
||||
// 1. BALANCE SYNC
|
||||
// ---------------------------
|
||||
const balData = await getBalanceData(account.accountId)
|
||||
|
||||
if (balData === false) break
|
||||
|
||||
if (balData) {
|
||||
const closing = balData.balances.find(
|
||||
(i: any) => i.balanceType === "closingBooked"
|
||||
)
|
||||
|
||||
const bookedBal = Number(closing.balanceAmount.amount)
|
||||
|
||||
await server.db
|
||||
.update(bankaccounts)
|
||||
.set({balance: bookedBal})
|
||||
.where(eq(bankaccounts.id, account.id))
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// 2. TRANSACTIONS
|
||||
// ---------------------------
|
||||
let transactions = await getTransactionData(account.accountId)
|
||||
if (!transactions) continue
|
||||
|
||||
//@ts-ignore
|
||||
transactions = transactions.map((item) => ({
|
||||
account: account.id,
|
||||
date: normalizeDate(item.bookingDate),
|
||||
credIban: item.creditorAccount?.iban ?? null,
|
||||
credName: item.creditorName ?? null,
|
||||
text: `
|
||||
${item.remittanceInformationUnstructured ?? ""}
|
||||
${item.remittanceInformationStructured ?? ""}
|
||||
${item.additionalInformation ?? ""}
|
||||
${item.remittanceInformationStructuredArray?.join("") ?? ""}
|
||||
`.trim(),
|
||||
amount: Number(item.transactionAmount.amount),
|
||||
tenant: account.tenant,
|
||||
debIban: item.debtorAccount?.iban ?? null,
|
||||
debName: item.debtorName ?? null,
|
||||
gocardlessId: item.internalTransactionId,
|
||||
currency: item.transactionAmount.currency,
|
||||
valueDate: normalizeDate(item.valueDate),
|
||||
}))
|
||||
|
||||
// Existierende Statements laden
|
||||
const existing = await server.db
|
||||
.select({gocardlessId: bankstatements.gocardlessId})
|
||||
.from(bankstatements)
|
||||
.where(eq(bankstatements.tenant, account.tenant))
|
||||
|
||||
const filtered = transactions.filter(
|
||||
//@ts-ignore
|
||||
(tx) => !existing.some((x) => x.gocardlessId === tx.gocardlessId)
|
||||
)
|
||||
|
||||
allNewTransactions.push(...filtered)
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// 3. NEW TRANSACTIONS → DB
|
||||
// ---------------------------
|
||||
if (allNewTransactions.length > 0) {
|
||||
await server.db.insert(bankstatements).values(allNewTransactions)
|
||||
|
||||
const affectedAccounts = [
|
||||
...new Set(allNewTransactions.map((t) => t.account)),
|
||||
]
|
||||
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
for (const accId of affectedAccounts) {
|
||||
await server.db
|
||||
.update(bankaccounts)
|
||||
//@ts-ignore
|
||||
.set({syncedAt: normalizeDate(dayjs())})
|
||||
.where(eq(bankaccounts.id, accId))
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Bank statement sync completed.")
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
run: async (tenant) => {
|
||||
await getToken()
|
||||
await syncAccounts(tenant)
|
||||
console.log("Service: Bankstatement sync finished")
|
||||
}
|
||||
}
|
||||
}
|
||||
259
src/modules/cron/dokuboximport.service.ts
Normal file
259
src/modules/cron/dokuboximport.service.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import axios from "axios"
|
||||
import dayjs from "dayjs"
|
||||
import { ImapFlow } from "imapflow"
|
||||
import { simpleParser } from "mailparser"
|
||||
import { FastifyInstance } from "fastify"
|
||||
|
||||
import {saveFile} from "../../utils/files";
|
||||
import { secrets } from "../../utils/secrets"
|
||||
|
||||
// Drizzle Imports
|
||||
import {
|
||||
tenants,
|
||||
folders,
|
||||
filetags,
|
||||
} from "../../../db/schema"
|
||||
|
||||
import {
|
||||
eq,
|
||||
and,
|
||||
} from "drizzle-orm"
|
||||
|
||||
let badMessageDetected = false
|
||||
let badMessageMessageSent = false
|
||||
|
||||
let client: ImapFlow | null = null
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// IMAP CLIENT INITIALIZEN
|
||||
// -------------------------------------------------------------
|
||||
export async function initDokuboxClient() {
|
||||
client = new ImapFlow({
|
||||
host: secrets.DOKUBOX_IMAP_HOST,
|
||||
port: secrets.DOKUBOX_IMAP_PORT,
|
||||
secure: secrets.DOKUBOX_IMAP_SECURE,
|
||||
auth: {
|
||||
user: secrets.DOKUBOX_IMAP_USER,
|
||||
pass: secrets.DOKUBOX_IMAP_PASSWORD
|
||||
},
|
||||
logger: false
|
||||
})
|
||||
|
||||
console.log("Dokubox E-Mail Client Initialized")
|
||||
|
||||
await client.connect()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// MAIN SYNC FUNCTION (DRIZZLE VERSION)
|
||||
// -------------------------------------------------------------
|
||||
export const syncDokubox = (server: FastifyInstance) =>
|
||||
async () => {
|
||||
|
||||
console.log("Perform Dokubox Sync")
|
||||
|
||||
await initDokuboxClient()
|
||||
|
||||
if (!client?.usable) {
|
||||
throw new Error("E-Mail Client not usable")
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// TENANTS LADEN (DRIZZLE)
|
||||
// -------------------------------
|
||||
const tenantList = await server.db
|
||||
.select({
|
||||
id: tenants.id,
|
||||
name: tenants.name,
|
||||
emailAddresses: tenants.dokuboxEmailAddresses,
|
||||
key: tenants.dokuboxkey
|
||||
})
|
||||
.from(tenants)
|
||||
|
||||
const lock = await client.getMailboxLock("INBOX")
|
||||
|
||||
try {
|
||||
|
||||
for await (let msg of client.fetch({ seen: false }, { envelope: true, source: true })) {
|
||||
|
||||
const parsed = await simpleParser(msg.source)
|
||||
|
||||
const message = {
|
||||
id: msg.uid,
|
||||
subject: parsed.subject,
|
||||
to: parsed.to?.value || [],
|
||||
cc: parsed.cc?.value || [],
|
||||
attachments: parsed.attachments || []
|
||||
}
|
||||
|
||||
// -------------------------------------------------
|
||||
// MAPPING / FIND TENANT
|
||||
// -------------------------------------------------
|
||||
const config = await getMessageConfigDrizzle(server, message, tenantList)
|
||||
|
||||
if (!config) {
|
||||
badMessageDetected = true
|
||||
|
||||
if (!badMessageMessageSent) {
|
||||
badMessageMessageSent = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (message.attachments.length > 0) {
|
||||
for (const attachment of message.attachments) {
|
||||
await saveFile(
|
||||
server,
|
||||
config.tenant,
|
||||
message.id,
|
||||
attachment,
|
||||
config.folder,
|
||||
config.filetype
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!badMessageDetected) {
|
||||
badMessageDetected = false
|
||||
badMessageMessageSent = false
|
||||
}
|
||||
|
||||
await client.messageFlagsAdd({ seen: false }, ["\\Seen"])
|
||||
await client.messageDelete({ seen: true })
|
||||
|
||||
} finally {
|
||||
lock.release()
|
||||
client.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// TENANT ERKENNEN + FOLDER/FILETYPES (DRIZZLE VERSION)
|
||||
// -------------------------------------------------------------
|
||||
const getMessageConfigDrizzle = async (
|
||||
server: FastifyInstance,
|
||||
message,
|
||||
tenantsList: any[]
|
||||
) => {
|
||||
|
||||
let possibleKeys: string[] = []
|
||||
|
||||
if (message.to) {
|
||||
message.to.forEach((item) =>
|
||||
possibleKeys.push(item.address.split("@")[0].toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
if (message.cc) {
|
||||
message.cc.forEach((item) =>
|
||||
possibleKeys.push(item.address.split("@")[0].toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// TENANT IDENTIFY
|
||||
// -------------------------------------------
|
||||
let tenant = tenantsList.find((t) => possibleKeys.includes(t.key))
|
||||
|
||||
if (!tenant && message.to?.length) {
|
||||
const address = message.to[0].address.toLowerCase()
|
||||
|
||||
tenant = tenantsList.find((t) =>
|
||||
(t.emailAddresses || []).map((m) => m.toLowerCase()).includes(address)
|
||||
)
|
||||
}
|
||||
|
||||
if (!tenant) return null
|
||||
|
||||
// -------------------------------------------
|
||||
// FOLDER + FILETYPE VIA SUBJECT
|
||||
// -------------------------------------------
|
||||
let folderId = null
|
||||
let filetypeId = null
|
||||
|
||||
// -------------------------------------------
|
||||
// Rechnung / Invoice
|
||||
// -------------------------------------------
|
||||
if (message.subject?.match(/(Rechnung|Beleg|Invoice|Quittung)/gi)) {
|
||||
|
||||
const folder = await server.db
|
||||
.select({ id: folders.id })
|
||||
.from(folders)
|
||||
.where(
|
||||
and(
|
||||
eq(folders.tenant, tenant.id),
|
||||
and(
|
||||
eq(folders.function, "incomingInvoices"),
|
||||
//@ts-ignore
|
||||
eq(folders.year, dayjs().format("YYYY"))
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
folderId = folder[0]?.id ?? null
|
||||
|
||||
const tag = await server.db
|
||||
.select({ id: filetags.id })
|
||||
.from(filetags)
|
||||
.where(
|
||||
and(
|
||||
eq(filetags.tenant, tenant.id),
|
||||
eq(filetags.incomingDocumentType, "invoices")
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
filetypeId = tag[0]?.id ?? null
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Mahnung
|
||||
// -------------------------------------------
|
||||
else if (message.subject?.match(/(Mahnung|Zahlungsaufforderung|Zahlungsverzug)/gi)) {
|
||||
|
||||
const tag = await server.db
|
||||
.select({ id: filetags.id })
|
||||
.from(filetags)
|
||||
.where(
|
||||
and(
|
||||
eq(filetags.tenant, tenant.id),
|
||||
eq(filetags.incomingDocumentType, "reminders")
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
filetypeId = tag[0]?.id ?? null
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Sonstige Dokumente → Deposit Folder
|
||||
// -------------------------------------------
|
||||
else {
|
||||
|
||||
const folder = await server.db
|
||||
.select({ id: folders.id })
|
||||
.from(folders)
|
||||
.where(
|
||||
and(
|
||||
eq(folders.tenant, tenant.id),
|
||||
eq(folders.function, "deposit")
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
folderId = folder[0]?.id ?? null
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
tenant: tenant.id,
|
||||
folder: folderId,
|
||||
filetype: filetypeId
|
||||
}
|
||||
}
|
||||
175
src/modules/cron/prepareIncomingInvoices.ts
Normal file
175
src/modules/cron/prepareIncomingInvoices.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import dayjs from "dayjs"
|
||||
import { getInvoiceDataFromGPT } from "../../utils/gpt"
|
||||
|
||||
// Drizzle schema
|
||||
import {
|
||||
tenants,
|
||||
files,
|
||||
filetags,
|
||||
incominginvoices,
|
||||
} from "../../../db/schema"
|
||||
|
||||
import { eq, and, isNull, not } from "drizzle-orm"
|
||||
|
||||
export function prepareIncomingInvoices(server: FastifyInstance) {
|
||||
const processInvoices = async (tenantId:number) => {
|
||||
console.log("▶ Starting Incoming Invoice Preparation")
|
||||
|
||||
const tenantsRes = await server.db
|
||||
.select()
|
||||
.from(tenants)
|
||||
.where(eq(tenants.id, tenantId))
|
||||
.orderBy(tenants.id)
|
||||
|
||||
if (!tenantsRes.length) {
|
||||
console.log("No tenants with autoPrepareIncomingInvoices = true")
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Processing tenants: ${tenantsRes.map(t => t.id).join(", ")}`)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 2️⃣ Jeden Tenant einzeln verarbeiten
|
||||
// -------------------------------------------------------------
|
||||
for (const tenant of tenantsRes) {
|
||||
const tenantId = tenant.id
|
||||
|
||||
// 2.1 Datei-Tags holen für incoming invoices
|
||||
const tagRes = await server.db
|
||||
.select()
|
||||
.from(filetags)
|
||||
.where(
|
||||
and(
|
||||
eq(filetags.tenant, tenantId),
|
||||
eq(filetags.incomingDocumentType, "invoices")
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
const invoiceFileTag = tagRes?.[0]?.id
|
||||
if (!invoiceFileTag) {
|
||||
server.log.error(`❌ Missing filetag 'invoices' for tenant ${tenantId}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 2.2 Alle Dateien laden, die als Invoice markiert sind aber NOCH keine incominginvoice haben
|
||||
const filesRes = await server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(
|
||||
and(
|
||||
eq(files.tenant, tenantId),
|
||||
eq(files.type, invoiceFileTag),
|
||||
isNull(files.incominginvoice),
|
||||
eq(files.archived, false),
|
||||
not(isNull(files.path))
|
||||
)
|
||||
)
|
||||
|
||||
if (!filesRes.length) {
|
||||
console.log(`No invoice files for tenant ${tenantId}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 3️⃣ Jede Datei einzeln durch GPT jagen & IncomingInvoice erzeugen
|
||||
// -------------------------------------------------------------
|
||||
for (const file of filesRes) {
|
||||
console.log(`Processing file ${file.id} for tenant ${tenantId}`)
|
||||
|
||||
const data = await getInvoiceDataFromGPT(server,file, tenantId)
|
||||
|
||||
if (!data) {
|
||||
server.log.warn(`GPT returned no data for file ${file.id}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3.1 IncomingInvoice-Objekt vorbereiten
|
||||
// ---------------------------------------------------------
|
||||
let itemInfo: any = {
|
||||
tenant: tenantId,
|
||||
state: "Vorbereitet"
|
||||
}
|
||||
|
||||
if (data.invoice_number) itemInfo.reference = data.invoice_number
|
||||
if (data.invoice_date) itemInfo.date = dayjs(data.invoice_date).toISOString()
|
||||
if (data.issuer?.id) itemInfo.vendor = data.issuer.id
|
||||
if (data.invoice_duedate) itemInfo.dueDate = dayjs(data.invoice_duedate).toISOString()
|
||||
|
||||
// Payment terms mapping
|
||||
const mapPayment: any = {
|
||||
"Direct Debit": "Einzug",
|
||||
"Transfer": "Überweisung",
|
||||
"Credit Card": "Kreditkarte",
|
||||
"Other": "Sonstiges",
|
||||
}
|
||||
if (data.terms) itemInfo.paymentType = mapPayment[data.terms] ?? data.terms
|
||||
|
||||
// 3.2 Positionszeilen konvertieren
|
||||
if (data.invoice_items?.length > 0) {
|
||||
itemInfo.accounts = data.invoice_items.map(item => ({
|
||||
account: item.account_id,
|
||||
description: item.description,
|
||||
amountNet: item.total_without_tax,
|
||||
amountTax: Number((item.total - item.total_without_tax).toFixed(2)),
|
||||
taxType: String(item.tax_rate),
|
||||
amountGross: item.total,
|
||||
costCentre: null,
|
||||
quantity: item.quantity,
|
||||
}))
|
||||
}
|
||||
|
||||
// 3.3 Beschreibung generieren
|
||||
let description = ""
|
||||
if (data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number}\n`
|
||||
if (data.reference) description += `Referenz: ${data.reference}\n`
|
||||
if (data.invoice_items) {
|
||||
for (const item of data.invoice_items) {
|
||||
description += `${item.description} - ${item.quantity} ${item.unit} - ${item.total}\n`
|
||||
}
|
||||
}
|
||||
itemInfo.description = description.trim()
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 4️⃣ IncomingInvoice erstellen
|
||||
// ---------------------------------------------------------
|
||||
const inserted = await server.db
|
||||
.insert(incominginvoices)
|
||||
.values(itemInfo)
|
||||
.returning()
|
||||
|
||||
const newInvoice = inserted?.[0]
|
||||
|
||||
if (!newInvoice) {
|
||||
server.log.error(`Failed to insert incoming invoice for file ${file.id}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 5️⃣ Datei mit incominginvoice-ID verbinden
|
||||
// ---------------------------------------------------------
|
||||
await server.db
|
||||
.update(files)
|
||||
.set({ incominginvoice: newInvoice.id })
|
||||
.where(eq(files.id, file.id))
|
||||
|
||||
console.log(`IncomingInvoice ${newInvoice.id} created for file ${file.id}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
run: async (tenant:number) => {
|
||||
await processInvoices(tenant)
|
||||
console.log("Incoming Invoice Preparation Completed.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
38
src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
38
src/modules/helpdesk/helpdesk.contact.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// modules/helpdesk/helpdesk.contact.service.ts
|
||||
import { FastifyInstance } from 'fastify'
|
||||
|
||||
export async function getOrCreateContact(
|
||||
server: FastifyInstance,
|
||||
tenant_id: number,
|
||||
{ email, phone, display_name, customer_id, contact_id }: { email?: string; phone?: string; display_name?: string; customer_id?: number; contact_id?: number }
|
||||
) {
|
||||
if (!email && !phone) throw new Error('Contact must have at least an email or phone')
|
||||
|
||||
// Bestehenden Kontakt prüfen
|
||||
const { data: existing, error: findError } = await server.supabase
|
||||
.from('helpdesk_contacts')
|
||||
.select('*')
|
||||
.eq('tenant_id', tenant_id)
|
||||
.or(`email.eq.${email || ''},phone.eq.${phone || ''}`)
|
||||
.maybeSingle()
|
||||
|
||||
if (findError) throw findError
|
||||
if (existing) return existing
|
||||
|
||||
// Anlegen
|
||||
const { data: created, error: insertError } = await server.supabase
|
||||
.from('helpdesk_contacts')
|
||||
.insert({
|
||||
tenant_id,
|
||||
email,
|
||||
phone,
|
||||
display_name,
|
||||
customer_id,
|
||||
contact_id
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (insertError) throw insertError
|
||||
return created
|
||||
}
|
||||
90
src/modules/helpdesk/helpdesk.conversation.service.ts
Normal file
90
src/modules/helpdesk/helpdesk.conversation.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// modules/helpdesk/helpdesk.conversation.service.ts
|
||||
import { FastifyInstance } from 'fastify'
|
||||
import { getOrCreateContact } from './helpdesk.contact.service.js'
|
||||
import {useNextNumberRangeNumber} from "../../utils/functions";
|
||||
|
||||
export async function createConversation(
|
||||
server: FastifyInstance,
|
||||
{
|
||||
tenant_id,
|
||||
contact,
|
||||
channel_instance_id,
|
||||
subject,
|
||||
customer_id = null,
|
||||
contact_person_id = null,
|
||||
}: {
|
||||
tenant_id: number
|
||||
contact: { email?: string; phone?: string; display_name?: string }
|
||||
channel_instance_id: string
|
||||
subject?: string,
|
||||
customer_id?: number,
|
||||
contact_person_id?: number
|
||||
}
|
||||
) {
|
||||
const contactRecord = await getOrCreateContact(server, tenant_id, contact)
|
||||
|
||||
const {usedNumber } = await useNextNumberRangeNumber(server, tenant_id, "tickets")
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('helpdesk_conversations')
|
||||
.insert({
|
||||
tenant_id,
|
||||
contact_id: contactRecord.id,
|
||||
channel_instance_id,
|
||||
subject: subject || null,
|
||||
status: 'open',
|
||||
created_at: new Date().toISOString(),
|
||||
customer_id,
|
||||
contact_person_id,
|
||||
ticket_number: usedNumber
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getConversations(
|
||||
server: FastifyInstance,
|
||||
tenant_id: number,
|
||||
opts?: { status?: string; limit?: number }
|
||||
) {
|
||||
const { status, limit = 50 } = opts || {}
|
||||
|
||||
let query = server.supabase.from('helpdesk_conversations').select('*, customer_id(*)').eq('tenant_id', tenant_id)
|
||||
|
||||
if (status) query = query.eq('status', status)
|
||||
query = query.order('last_message_at', { ascending: false }).limit(limit)
|
||||
|
||||
const { data, error } = await query
|
||||
if (error) throw error
|
||||
|
||||
const mappedData = data.map(entry => {
|
||||
return {
|
||||
...entry,
|
||||
customer: entry.customer_id
|
||||
}
|
||||
})
|
||||
|
||||
return mappedData
|
||||
}
|
||||
|
||||
export async function updateConversationStatus(
|
||||
server: FastifyInstance,
|
||||
conversation_id: string,
|
||||
status: string
|
||||
) {
|
||||
const valid = ['open', 'in_progress', 'waiting_for_customer', 'answered', 'closed']
|
||||
if (!valid.includes(status)) throw new Error('Invalid status')
|
||||
|
||||
const { data, error } = await server.supabase
|
||||
.from('helpdesk_conversations')
|
||||
.update({ status })
|
||||
.eq('id', conversation_id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
60
src/modules/helpdesk/helpdesk.message.service.ts
Normal file
60
src/modules/helpdesk/helpdesk.message.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// modules/helpdesk/helpdesk.message.service.ts
|
||||
import { FastifyInstance } from 'fastify'
|
||||
|
||||
export async function addMessage(
|
||||
server: FastifyInstance,
|
||||
{
|
||||
tenant_id,
|
||||
conversation_id,
|
||||
author_user_id = null,
|
||||
direction = 'incoming',
|
||||
payload,
|
||||
raw_meta = null,
|
||||
external_message_id = null,
|
||||
}: {
|
||||
tenant_id: number
|
||||
conversation_id: string
|
||||
author_user_id?: string | null
|
||||
direction?: 'incoming' | 'outgoing' | 'internal' | 'system'
|
||||
payload: any
|
||||
raw_meta?: any
|
||||
external_message_id?: string
|
||||
}
|
||||
) {
|
||||
if (!payload?.text) throw new Error('Message payload requires text content')
|
||||
|
||||
const { data: message, error } = await server.supabase
|
||||
.from('helpdesk_messages')
|
||||
.insert({
|
||||
tenant_id,
|
||||
conversation_id,
|
||||
author_user_id,
|
||||
direction,
|
||||
payload,
|
||||
raw_meta,
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
// Letzte Nachricht aktualisieren
|
||||
await server.supabase
|
||||
.from('helpdesk_conversations')
|
||||
.update({ last_message_at: new Date().toISOString() })
|
||||
.eq('id', conversation_id)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
export async function getMessages(server: FastifyInstance, conversation_id: string) {
|
||||
const { data, error } = await server.supabase
|
||||
.from('helpdesk_messages')
|
||||
.select('*')
|
||||
.eq('conversation_id', conversation_id)
|
||||
.order('created_at', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
148
src/modules/notification.service.ts
Normal file
148
src/modules/notification.service.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// services/notification.service.ts
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import {secrets} from "../utils/secrets";
|
||||
|
||||
export type NotificationStatus = 'queued' | 'sent' | 'failed';
|
||||
|
||||
export interface TriggerInput {
|
||||
tenantId: number;
|
||||
userId: string; // muss auf public.auth_users.id zeigen
|
||||
eventType: string; // muss in notifications_event_types existieren
|
||||
title: string; // Betreff/Title
|
||||
message: string; // Klartext-Inhalt
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UserDirectoryInfo {
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export type UserDirectory = (server: FastifyInstance, userId: string, tenantId: number) => Promise<UserDirectoryInfo | null>;
|
||||
|
||||
export class NotificationService {
|
||||
constructor(
|
||||
private server: FastifyInstance,
|
||||
private getUser: UserDirectory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Löst eine E-Mail-Benachrichtigung aus:
|
||||
* - Validiert den Event-Typ
|
||||
* - Legt einen Datensatz in notifications_items an (status: queued)
|
||||
* - Versendet E-Mail (FEDEO Branding)
|
||||
* - Aktualisiert status/sent_at bzw. error
|
||||
*/
|
||||
async trigger(input: TriggerInput) {
|
||||
const { tenantId, userId, eventType, title, message, payload } = input;
|
||||
const supabase = this.server.supabase;
|
||||
|
||||
// 1) Event-Typ prüfen (aktiv?)
|
||||
const { data: eventTypeRow, error: etErr } = await supabase
|
||||
.from('notifications_event_types')
|
||||
.select('event_key,is_active')
|
||||
.eq('event_key', eventType)
|
||||
.maybeSingle();
|
||||
|
||||
if (etErr || !eventTypeRow || eventTypeRow.is_active !== true) {
|
||||
throw new Error(`Unbekannter oder inaktiver Event-Typ: ${eventType}`);
|
||||
}
|
||||
|
||||
// 2) Zieladresse beschaffen
|
||||
const user = await this.getUser(this.server, userId, tenantId);
|
||||
if (!user?.email) {
|
||||
throw new Error(`Nutzer ${userId} hat keine E-Mail-Adresse`);
|
||||
}
|
||||
|
||||
// 3) Notification anlegen (status: queued)
|
||||
const { data: inserted, error: insErr } = await supabase
|
||||
.from('notifications_items')
|
||||
.insert({
|
||||
tenant_id: tenantId,
|
||||
user_id: userId,
|
||||
event_type: eventType,
|
||||
title,
|
||||
message,
|
||||
payload: payload ?? null,
|
||||
channel: 'email',
|
||||
status: 'queued'
|
||||
})
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (insErr || !inserted) {
|
||||
throw new Error(`Fehler beim Einfügen der Notification: ${insErr?.message}`);
|
||||
}
|
||||
|
||||
// 4) E-Mail versenden
|
||||
try {
|
||||
await this.sendEmail(user.email, title, message);
|
||||
|
||||
await supabase
|
||||
.from('notifications_items')
|
||||
.update({ status: 'sent', sent_at: new Date().toISOString() })
|
||||
.eq('id', inserted.id);
|
||||
|
||||
return { success: true, id: inserted.id };
|
||||
} catch (err: any) {
|
||||
await supabase
|
||||
.from('notifications_items')
|
||||
.update({ status: 'failed', error: String(err?.message || err) })
|
||||
.eq('id', inserted.id);
|
||||
|
||||
this.server.log.error({ err, notificationId: inserted.id }, 'E-Mail Versand fehlgeschlagen');
|
||||
return { success: false, error: err?.message || 'E-Mail Versand fehlgeschlagen' };
|
||||
}
|
||||
}
|
||||
|
||||
// ---- private helpers ------------------------------------------------------
|
||||
|
||||
private async sendEmail(to: string, subject: string, message: string) {
|
||||
const nodemailer = await import('nodemailer');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: secrets.MAILER_SMTP_HOST,
|
||||
port: Number(secrets.MAILER_SMTP_PORT),
|
||||
secure: secrets.MAILER_SMTP_SSL === 'true',
|
||||
auth: {
|
||||
user: secrets.MAILER_SMTP_USER,
|
||||
pass: secrets.MAILER_SMTP_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const html = this.renderFedeoHtml(subject, message);
|
||||
|
||||
await transporter.sendMail({
|
||||
from: secrets.MAILER_FROM,
|
||||
to,
|
||||
subject,
|
||||
text: message,
|
||||
html
|
||||
});
|
||||
}
|
||||
|
||||
private renderFedeoHtml(title: string, message: string) {
|
||||
return `
|
||||
<html><body style="font-family:sans-serif;color:#222">
|
||||
<div style="border:1px solid #ddd;border-radius:8px;padding:16px;max-width:600px;margin:auto">
|
||||
<h2 style="color:#0f62fe;margin:0 0 12px">FEDEO</h2>
|
||||
<h3 style="margin:0 0 8px">${this.escapeHtml(title)}</h3>
|
||||
<p>${this.nl2br(this.escapeHtml(message))}</p>
|
||||
<hr style="margin:16px 0;border:none;border-top:1px solid #eee"/>
|
||||
<p style="font-size:12px;color:#666">Automatisch generiert von FEDEO</p>
|
||||
</div>
|
||||
</body></html>
|
||||
`;
|
||||
}
|
||||
|
||||
// simple escaping (ausreichend für unser Template)
|
||||
private escapeHtml(s: string) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
private nl2br(s: string) {
|
||||
return s.replace(/\n/g, '<br/>');
|
||||
}
|
||||
}
|
||||
406
src/modules/publiclinks.service.ts
Normal file
406
src/modules/publiclinks.service.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import bcrypt from 'bcrypt';
|
||||
import crypto from 'crypto';
|
||||
import {and, eq, inArray, not} from 'drizzle-orm';
|
||||
import * as schema from '../../db/schema';
|
||||
import {useNextNumberRangeNumber} from "../utils/functions"; // Pfad anpassen
|
||||
|
||||
|
||||
export const publicLinkService = {
|
||||
|
||||
/**
|
||||
* Erstellt einen neuen Public Link
|
||||
*/
|
||||
async createLink(server: FastifyInstance, tenantId: number,
|
||||
name: string,
|
||||
isProtected?: boolean,
|
||||
pin?: string,
|
||||
customToken?: string,
|
||||
config?: Record<string, any>,
|
||||
defaultProfileId?: string) {
|
||||
let pinHash: string | null = null;
|
||||
|
||||
// 1. PIN Hashen, falls Schutz aktiviert ist
|
||||
if (isProtected && pin) {
|
||||
pinHash = await bcrypt.hash(pin, 10);
|
||||
} else if (isProtected && !pin) {
|
||||
throw new Error("Für geschützte Links muss eine PIN angegeben werden.");
|
||||
}
|
||||
|
||||
// 2. Token generieren oder Custom Token verwenden
|
||||
let token = customToken;
|
||||
|
||||
if (!token) {
|
||||
// Generiere einen zufälligen Token (z.B. hex string)
|
||||
// Alternativ: nanoid nutzen, falls installiert
|
||||
token = crypto.randomBytes(12).toString('hex');
|
||||
}
|
||||
|
||||
// Prüfen, ob Token schon existiert (nur bei Custom Token wichtig)
|
||||
if (customToken) {
|
||||
const existing = await server.db.query.publicLinks.findFirst({
|
||||
where: eq(schema.publicLinks.token, token)
|
||||
});
|
||||
if (existing) {
|
||||
throw new Error(`Der Token '${token}' ist bereits vergeben.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. DB Insert
|
||||
const [newLink] = await server.db.insert(schema.publicLinks).values({
|
||||
tenant: tenantId,
|
||||
name: name,
|
||||
token: token,
|
||||
isProtected: isProtected || false,
|
||||
pinHash: pinHash,
|
||||
config: config || {},
|
||||
defaultProfile: defaultProfileId || null,
|
||||
active: true
|
||||
}).returning();
|
||||
|
||||
return newLink;
|
||||
},
|
||||
|
||||
/**
|
||||
* Listet alle Links für einen Tenant auf (für die Verwaltungs-UI später)
|
||||
*/
|
||||
async getLinksByTenant(server: FastifyInstance, tenantId: number) {
|
||||
return server.db.select()
|
||||
.from(schema.publicLinks)
|
||||
.where(eq(schema.publicLinks.tenant, tenantId));
|
||||
},
|
||||
|
||||
|
||||
async getLinkContext(server: FastifyInstance, token: string, providedPin?: string) {
|
||||
// 1. Link laden & Checks
|
||||
const linkConfig = await server.db.query.publicLinks.findFirst({
|
||||
where: eq(schema.publicLinks.token, token)
|
||||
});
|
||||
|
||||
if (!linkConfig || !linkConfig.active) throw new Error("Link_NotFound");
|
||||
|
||||
// 2. PIN Check
|
||||
if (linkConfig.isProtected) {
|
||||
if (!providedPin) throw new Error("Pin_Required");
|
||||
const isValid = await bcrypt.compare(providedPin, linkConfig.pinHash || "");
|
||||
if (!isValid) throw new Error("Pin_Invalid");
|
||||
}
|
||||
|
||||
const tenantId = linkConfig.tenant;
|
||||
const config = linkConfig.config as any;
|
||||
|
||||
// --- RESSOURCEN & FILTER ---
|
||||
|
||||
// Standardmäßig alles laden, wenn 'resources' nicht definiert ist
|
||||
const requestedResources: string[] = config.resources || ['profiles', 'projects', 'services', 'units'];
|
||||
const filters = config.filters || {}; // Erwartet jetzt: { projects: [1,2], services: [3,4] }
|
||||
|
||||
const queryPromises: Record<string, Promise<any[]>> = {};
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 1. PROFILES (Mitarbeiter)
|
||||
// ---------------------------------------------------------
|
||||
if (requestedResources.includes('profiles')) {
|
||||
let profileCondition = and(
|
||||
eq(schema.authProfiles.tenant_id, tenantId),
|
||||
eq(schema.authProfiles.active, true)
|
||||
);
|
||||
|
||||
// Sicherheits-Feature: Default Profil erzwingen
|
||||
if (linkConfig.defaultProfile) {
|
||||
profileCondition = and(profileCondition, eq(schema.authProfiles.id, linkConfig.defaultProfile));
|
||||
}
|
||||
|
||||
// Optional: Auch hier Filter ermöglichen (falls man z.B. nur 3 bestimmte MA zur Auswahl geben will)
|
||||
if (filters.profiles && Array.isArray(filters.profiles) && filters.profiles.length > 0) {
|
||||
profileCondition = and(profileCondition, inArray(schema.authProfiles.id, filters.profiles));
|
||||
}
|
||||
|
||||
queryPromises.profiles = server.db.select({
|
||||
id: schema.authProfiles.id,
|
||||
fullName: schema.authProfiles.full_name
|
||||
})
|
||||
.from(schema.authProfiles)
|
||||
.where(profileCondition);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2. PROJECTS (Aufträge)
|
||||
// ---------------------------------------------------------
|
||||
if (requestedResources.includes('projects')) {
|
||||
let projectCondition = and(
|
||||
eq(schema.projects.tenant, tenantId),
|
||||
not(eq(schema.projects.active_phase, 'Abgeschlossen'))
|
||||
);
|
||||
|
||||
// NEU: Zugriff direkt auf filters.projects
|
||||
if (filters.projects && Array.isArray(filters.projects) && filters.projects.length > 0) {
|
||||
projectCondition = and(projectCondition, inArray(schema.projects.id, filters.projects));
|
||||
}
|
||||
|
||||
queryPromises.projects = server.db.select({
|
||||
id: schema.projects.id,
|
||||
name: schema.projects.name
|
||||
})
|
||||
.from(schema.projects)
|
||||
.where(projectCondition);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3. SERVICES (Tätigkeiten)
|
||||
// ---------------------------------------------------------
|
||||
if (requestedResources.includes('services')) {
|
||||
let serviceCondition = eq(schema.services.tenant, tenantId);
|
||||
|
||||
// NEU: Zugriff direkt auf filters.services
|
||||
if (filters.services && Array.isArray(filters.services) && filters.services.length > 0) {
|
||||
serviceCondition = and(serviceCondition, inArray(schema.services.id, filters.services));
|
||||
}
|
||||
|
||||
queryPromises.services = server.db.select({
|
||||
id: schema.services.id,
|
||||
name: schema.services.name,
|
||||
unit: schema.services.unit
|
||||
})
|
||||
.from(schema.services)
|
||||
.where(serviceCondition);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 4. UNITS (Einheiten)
|
||||
// ---------------------------------------------------------
|
||||
if (requestedResources.includes('units')) {
|
||||
// Units werden meist global geladen, könnten aber auch gefiltert werden
|
||||
queryPromises.units = server.db.select().from(schema.units);
|
||||
}
|
||||
|
||||
// --- QUERY AUSFÜHRUNG ---
|
||||
const results = await Promise.all(Object.values(queryPromises));
|
||||
const keys = Object.keys(queryPromises);
|
||||
|
||||
const dataResponse: Record<string, any[]> = {
|
||||
profiles: [],
|
||||
projects: [],
|
||||
services: [],
|
||||
units: []
|
||||
};
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
dataResponse[key] = results[index];
|
||||
});
|
||||
|
||||
return {
|
||||
config: linkConfig.config,
|
||||
meta: {
|
||||
formName: linkConfig.name,
|
||||
defaultProfileId: linkConfig.defaultProfile
|
||||
},
|
||||
data: dataResponse
|
||||
};
|
||||
},
|
||||
|
||||
async submitFormData(server: FastifyInstance, token: string, payload: any, providedPin?: string) {
|
||||
// 1. Validierung (Token & PIN)
|
||||
const linkConfig = await server.db.query.publicLinks.findFirst({
|
||||
where: eq(schema.publicLinks.token, token)
|
||||
});
|
||||
|
||||
if (!linkConfig || !linkConfig.active) throw new Error("Link_NotFound");
|
||||
|
||||
if (linkConfig.isProtected) {
|
||||
if (!providedPin) throw new Error("Pin_Required");
|
||||
const isValid = await bcrypt.compare(providedPin, linkConfig.pinHash || "");
|
||||
if (!isValid) throw new Error("Pin_Invalid");
|
||||
}
|
||||
|
||||
const tenantId = linkConfig.tenant;
|
||||
const config = linkConfig.config as any;
|
||||
|
||||
// 2. USER ID AUFLÖSEN
|
||||
// Wir holen die profileId aus dem Link (Default) oder dem Payload (User-Auswahl)
|
||||
const rawProfileId = linkConfig.defaultProfile || payload.profile;
|
||||
if (!rawProfileId) throw new Error("Profile_Missing");
|
||||
|
||||
// Profil laden, um die user_id zu bekommen
|
||||
const authProfile = await server.db.query.authProfiles.findFirst({
|
||||
where: eq(schema.authProfiles.id, rawProfileId)
|
||||
});
|
||||
|
||||
if (!authProfile) throw new Error("Profile_Not_Found");
|
||||
|
||||
// Da du sagtest, es gibt immer einen User, verlassen wir uns darauf.
|
||||
// Falls null, werfen wir einen Fehler, da die DB sonst beim Insert knallt.
|
||||
const userId = authProfile.user_id;
|
||||
if (!userId) throw new Error("Profile_Has_No_User_Assigned");
|
||||
|
||||
|
||||
// Helper für Datum
|
||||
const normalizeDate = (val: any) => {
|
||||
if (!val) return null
|
||||
const d = new Date(val)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT A: Stammdaten laden
|
||||
// =================================================================
|
||||
const project = await server.db.query.projects.findFirst({
|
||||
where: eq(schema.projects.id, payload.project)
|
||||
});
|
||||
if (!project) throw new Error("Project not found");
|
||||
|
||||
const customer = await server.db.query.customers.findFirst({
|
||||
where: eq(schema.customers.id, project.customer)
|
||||
});
|
||||
|
||||
const service = await server.db.query.services.findFirst({
|
||||
where: eq(schema.services.id, payload.service)
|
||||
});
|
||||
if (!service) throw new Error("Service not found");
|
||||
|
||||
// Texttemplates & Letterhead laden
|
||||
const texttemplates = await server.db.query.texttemplates.findMany({
|
||||
where: (t, {and, eq}) => and(
|
||||
eq(t.tenant, tenantId),
|
||||
eq(t.documentType, 'deliveryNotes')
|
||||
)
|
||||
});
|
||||
const letterhead = await server.db.query.letterheads.findFirst({
|
||||
where: eq(schema.letterheads.tenant, tenantId)
|
||||
});
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT B: Nummernkreis generieren
|
||||
// =================================================================
|
||||
const {usedNumber} = await useNextNumberRangeNumber(server, tenantId, "deliveryNotes");
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT C: Berechnungen
|
||||
// =================================================================
|
||||
const startDate = normalizeDate(payload.startDate) || new Date();
|
||||
let endDate = normalizeDate(payload.endDate);
|
||||
|
||||
// Fallback Endzeit (+1h)
|
||||
if (!endDate) {
|
||||
endDate = server.dayjs(startDate).add(1, 'hour').toDate();
|
||||
}
|
||||
|
||||
// Menge berechnen
|
||||
let quantity = payload.quantity;
|
||||
if (!quantity && payload.totalHours) quantity = payload.totalHours;
|
||||
if (!quantity) {
|
||||
const diffMin = server.dayjs(endDate).diff(server.dayjs(startDate), 'minute');
|
||||
quantity = Number((diffMin / 60).toFixed(2));
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT D: Lieferschein erstellen
|
||||
// =================================================================
|
||||
const createDocData = {
|
||||
tenant: tenantId,
|
||||
type: "deliveryNotes",
|
||||
state: "Entwurf",
|
||||
customer: project.customer,
|
||||
//@ts-ignore
|
||||
address: customer ? {zip: customer.infoData.zip, city: customer.infoData.city, street: customer.infoData.street,} : {},
|
||||
project: project.id,
|
||||
documentNumber: usedNumber,
|
||||
documentDate: String(new Date()), // Schema sagt 'text', evtl toISOString() besser?
|
||||
deliveryDate: String(startDate), // Schema sagt 'text'
|
||||
deliveryDateType: "Leistungsdatum",
|
||||
createdBy: userId, // WICHTIG: Hier die User ID
|
||||
created_by: userId, // WICHTIG: Hier die User ID
|
||||
title: "Lieferschein",
|
||||
description: "",
|
||||
startText: texttemplates.find((i: any) => i.default && i.pos === "startText")?.text || "",
|
||||
endText: texttemplates.find((i: any) => i.default && i.pos === "endText")?.text || "",
|
||||
rows: [
|
||||
{
|
||||
pos: "1",
|
||||
mode: "service",
|
||||
service: service.id,
|
||||
text: service.name,
|
||||
unit: service.unit,
|
||||
quantity: quantity,
|
||||
description: service.description || null,
|
||||
descriptionText: service.description || null,
|
||||
agriculture: {
|
||||
dieselUsage: payload.dieselUsage || null,
|
||||
}
|
||||
}
|
||||
],
|
||||
contactPerson: userId, // WICHTIG: Hier die User ID
|
||||
letterhead: letterhead?.id,
|
||||
};
|
||||
|
||||
const [createdDoc] = await server.db.insert(schema.createddocuments)
|
||||
//@ts-ignore
|
||||
.values(createDocData)
|
||||
.returning();
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT E: Zeiterfassung Events
|
||||
// =================================================================
|
||||
if (config.features?.timeTracking) {
|
||||
|
||||
// Metadaten für das Event
|
||||
const eventMetadata = {
|
||||
project: project.id,
|
||||
service: service.id,
|
||||
description: payload.description || "",
|
||||
generatedDocumentId: createdDoc.id
|
||||
};
|
||||
|
||||
// 1. START EVENT
|
||||
await server.db.insert(schema.stafftimeevents).values({
|
||||
tenant_id: tenantId,
|
||||
user_id: userId, // WICHTIG: User ID
|
||||
actortype: "user",
|
||||
actoruser_id: userId, // WICHTIG: User ID
|
||||
eventtime: startDate,
|
||||
eventtype: "START",
|
||||
source: "PUBLIC_LINK",
|
||||
metadata: eventMetadata // WICHTIG: Schema heißt 'metadata', nicht 'payload'
|
||||
});
|
||||
|
||||
// 2. STOP EVENT
|
||||
await server.db.insert(schema.stafftimeevents).values({
|
||||
tenant_id: tenantId,
|
||||
user_id: userId,
|
||||
actortype: "user",
|
||||
actoruser_id: userId,
|
||||
eventtime: endDate,
|
||||
eventtype: "STOP",
|
||||
source: "PUBLIC_LINK",
|
||||
metadata: eventMetadata
|
||||
});
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// SCHRITT F: History Items
|
||||
// =================================================================
|
||||
const historyItemsToCreate = [];
|
||||
|
||||
if (payload.description) {
|
||||
historyItemsToCreate.push({
|
||||
tenant: tenantId,
|
||||
createdBy: userId, // WICHTIG: User ID
|
||||
text: `Notiz aus Webformular Lieferschein ${createdDoc.documentNumber}: ${payload.description}`,
|
||||
project: project.id,
|
||||
createdDocument: createdDoc.id
|
||||
});
|
||||
}
|
||||
|
||||
historyItemsToCreate.push({
|
||||
tenant: tenantId,
|
||||
createdBy: userId, // WICHTIG: User ID
|
||||
text: `Webformular abgeschickt. Lieferschein ${createdDoc.documentNumber} erstellt. Zeit gebucht (Start/Stop).`,
|
||||
project: project.id,
|
||||
createdDocument: createdDoc.id
|
||||
});
|
||||
|
||||
await server.db.insert(schema.historyitems).values(historyItemsToCreate);
|
||||
|
||||
return {success: true, documentNumber: createdDoc.documentNumber};
|
||||
}
|
||||
}
|
||||
725
src/modules/serialexecution.service.ts
Normal file
725
src/modules/serialexecution.service.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
import dayjs from "dayjs";
|
||||
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
||||
import Handlebars from "handlebars";
|
||||
import axios from "axios";
|
||||
import { eq, inArray, and } from "drizzle-orm"; // Drizzle Operatoren
|
||||
|
||||
// DEINE IMPORTS
|
||||
import * as schema from "../../db/schema"; // Importiere dein Drizzle Schema
|
||||
import { saveFile } from "../utils/files";
|
||||
import {FastifyInstance} from "fastify";
|
||||
import {useNextNumberRangeNumber} from "../utils/functions";
|
||||
import {createInvoicePDF} from "../utils/pdf"; // Achtung: Muss Node.js Buffer unterstützen!
|
||||
|
||||
dayjs.extend(quarterOfYear);
|
||||
|
||||
|
||||
export const executeManualGeneration = async (server:FastifyInstance,executionDate,templateIds,tenantId,executedBy) => {
|
||||
try {
|
||||
console.log(executedBy)
|
||||
|
||||
const executionDayjs = dayjs(executionDate);
|
||||
|
||||
console.log(`Starte manuelle Generierung für Tenant ${tenantId} am ${executionDate}`);
|
||||
|
||||
// 1. Tenant laden (Drizzle)
|
||||
// Wir nehmen an, dass 'tenants' im Schema definiert ist
|
||||
const [tenant] = await server.db
|
||||
.select()
|
||||
.from(schema.tenants)
|
||||
.where(eq(schema.tenants.id, tenantId))
|
||||
.limit(1);
|
||||
|
||||
if (!tenant) throw new Error(`Tenant mit ID ${tenantId} nicht gefunden.`);
|
||||
|
||||
// 2. Templates laden
|
||||
const templates = await server.db
|
||||
.select()
|
||||
.from(schema.createddocuments)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.createddocuments.tenant, tenantId),
|
||||
eq(schema.createddocuments.type, "serialInvoices"),
|
||||
inArray(schema.createddocuments.id, templateIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (templates.length === 0) {
|
||||
console.warn("Keine passenden Vorlagen gefunden.");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Folder & FileType IDs holen (Hilfsfunktionen unten)
|
||||
const folderId = await getFolderId(server,tenantId);
|
||||
const fileTypeId = await getFileTypeId(server,tenantId);
|
||||
|
||||
const results = [];
|
||||
|
||||
const [executionRecord] = await server.db
|
||||
.insert(schema.serialExecutions)
|
||||
.values({
|
||||
tenant: tenantId,
|
||||
executionDate: executionDayjs.toDate(),
|
||||
status: "draft",
|
||||
createdBy: executedBy,
|
||||
summary: `${templateIds.length} Vorlagen verarbeitet`
|
||||
})
|
||||
.returning();
|
||||
|
||||
console.log(executionRecord);
|
||||
|
||||
// 4. Loop durch die Templates
|
||||
for (const template of templates) {
|
||||
try {
|
||||
const resultId = await processSingleTemplate(
|
||||
server,
|
||||
template,
|
||||
tenant,
|
||||
executionDayjs,
|
||||
folderId,
|
||||
fileTypeId,
|
||||
executedBy,
|
||||
executionRecord.id
|
||||
);
|
||||
results.push({ id: template.id, status: "success", newDocumentId: resultId });
|
||||
} catch (e: any) {
|
||||
console.error(`Fehler bei Template ${template.id}:`, e);
|
||||
results.push({ id: template.id, status: "error", error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
export const finishManualGeneration = async (server: FastifyInstance, executionId: number) => {
|
||||
try {
|
||||
console.log(`Beende Ausführung ${executionId}...`);
|
||||
|
||||
// 1. Execution und Tenant laden
|
||||
|
||||
const [executionRecord] = await server.db
|
||||
.select()
|
||||
.from(schema.serialExecutions)// @ts-ignore
|
||||
.where(eq(schema.serialExecutions.id, executionId))
|
||||
.limit(1);
|
||||
|
||||
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
||||
|
||||
console.log(executionRecord);
|
||||
|
||||
const tenantId = executionRecord.tenant;
|
||||
|
||||
console.log(tenantId)
|
||||
|
||||
// Tenant laden (für Settings etc.)
|
||||
const [tenant] = await server.db
|
||||
.select()
|
||||
.from(schema.tenants)
|
||||
.where(eq(schema.tenants.id, tenantId))
|
||||
.limit(1);
|
||||
|
||||
// 2. Status auf "processing" setzen (optional, damit UI feedback hat)
|
||||
|
||||
/*await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({ status: "processing" })// @ts-ignore
|
||||
.where(eq(schema.serialExecutions.id, executionId));*/
|
||||
|
||||
// 3. Alle erstellten Dokumente dieser Execution laden
|
||||
const documents = await server.db
|
||||
.select()
|
||||
.from(schema.createddocuments)
|
||||
.where(eq(schema.createddocuments.serialexecution, executionId));
|
||||
|
||||
console.log(`${documents.length} Dokumente werden finalisiert...`);
|
||||
|
||||
// 4. IDs für File-System laden (nur einmalig nötig)
|
||||
const folderId = await getFolderId(server, tenantId);
|
||||
const fileTypeId = await getFileTypeId(server, tenantId);
|
||||
|
||||
// Globale Daten laden, die für alle gleich sind (Optimierung)
|
||||
const [units, products, services] = await Promise.all([
|
||||
server.db.select().from(schema.units),
|
||||
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenantId)),
|
||||
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenantId)),
|
||||
]);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// 5. Loop durch Dokumente
|
||||
for (const doc of documents) {
|
||||
try {
|
||||
|
||||
const [letterhead] = await Promise.all([
|
||||
/*fetchById(server, schema.contacts, doc.contact),
|
||||
fetchById(server, schema.customers, doc.customer),
|
||||
fetchById(server, schema.authProfiles, doc.contactPerson), // oder createdBy, je nach Logik
|
||||
fetchById(server, schema.projects, doc.project),
|
||||
fetchById(server, schema.contracts, doc.contract),*/
|
||||
doc.letterhead ? fetchById(server, schema.letterheads, doc.letterhead) : null
|
||||
]);
|
||||
|
||||
const pdfData = await getCloseData(
|
||||
server,
|
||||
doc,
|
||||
tenant,
|
||||
units,
|
||||
products,
|
||||
services,
|
||||
);
|
||||
|
||||
console.log(pdfData);
|
||||
|
||||
// D. PDF Generieren
|
||||
const pdfBase64 = await createInvoicePDF(server,"base64",pdfData, letterhead?.path);
|
||||
|
||||
console.log(pdfBase64);
|
||||
|
||||
// E. Datei speichern
|
||||
// @ts-ignore
|
||||
const fileBuffer = Buffer.from(pdfBase64.base64, "base64");
|
||||
const filename = `${pdfData.documentNumber}.pdf`;
|
||||
|
||||
await saveFile(
|
||||
server,
|
||||
tenantId,
|
||||
null, // User ID (hier ggf. executionRecord.createdBy nutzen wenn verfügbar)
|
||||
fileBuffer,
|
||||
folderId,
|
||||
fileTypeId,
|
||||
{
|
||||
createddocument: doc.id,
|
||||
filename: filename,
|
||||
filesize: fileBuffer.length // Falls saveFile das braucht
|
||||
}
|
||||
);
|
||||
|
||||
// F. Dokument in DB final updaten
|
||||
await server.db
|
||||
.update(schema.createddocuments)
|
||||
.set({
|
||||
state: "Gebucht",
|
||||
documentNumber: pdfData.documentNumber,
|
||||
title: pdfData.title,
|
||||
pdf_path: filename // Optional, falls du den Pfad direkt am Doc speicherst
|
||||
})
|
||||
.where(eq(schema.createddocuments.id, doc.id));
|
||||
|
||||
successCount++;
|
||||
|
||||
} catch (innerErr) {
|
||||
console.error(`Fehler beim Finalisieren von Doc ID ${doc.id}:`, innerErr);
|
||||
errorCount++;
|
||||
// Optional: Status des einzelnen Dokuments auf Error setzen
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Execution abschließen
|
||||
const finalStatus = errorCount > 0 ? "error" : "completed"; // Oder 'completed' auch wenn Teilerfolge
|
||||
|
||||
|
||||
await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({
|
||||
status: finalStatus,
|
||||
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
||||
})// @ts-ignore
|
||||
.where(eq(schema.serialExecutions.id, executionId));
|
||||
|
||||
return { success: true, processed: successCount, errors: errorCount };
|
||||
|
||||
} catch (error) {
|
||||
console.error("Critical Error in finishManualGeneration:", error);
|
||||
// Execution auf Error setzen
|
||||
// @ts-ignore
|
||||
await server.db
|
||||
.update(schema.serialExecutions)
|
||||
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
||||
//@ts-ignore
|
||||
.where(eq(schema.serialExecutions.id, executionId));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verarbeitet eine einzelne Vorlage
|
||||
*/
|
||||
async function processSingleTemplate(server: FastifyInstance, template: any, tenant: any,executionDate: dayjs.Dayjs,folderId: string,fileTypeId: string,executedBy: string,executionId: string) {
|
||||
// A. Zugehörige Daten parallel laden
|
||||
const [contact, customer, profile, project, contract, units, products, services, letterhead] = await Promise.all([
|
||||
fetchById(server, schema.contacts, template.contact),
|
||||
fetchById(server, schema.customers, template.customer),
|
||||
fetchById(server, schema.authProfiles, template.contactPerson),
|
||||
fetchById(server, schema.projects, template.project),
|
||||
fetchById(server, schema.contracts, template.contract),
|
||||
server.db.select().from(schema.units),
|
||||
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenant.id)),
|
||||
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenant.id)),
|
||||
template.letterhead ? fetchById(server, schema.letterheads, template.letterhead) : null
|
||||
]);
|
||||
|
||||
// B. Datumsberechnung (Logik aus dem Original)
|
||||
const { firstDate, lastDate } = calculateDateRange(template.serialConfig, executionDate);
|
||||
|
||||
// C. Rechnungsnummer & Save Data
|
||||
const savePayload = await getSaveData(
|
||||
template,
|
||||
tenant,
|
||||
firstDate,
|
||||
lastDate,
|
||||
executionDate.toISOString(),
|
||||
executedBy
|
||||
);
|
||||
|
||||
const payloadWithRelation = {
|
||||
...savePayload,
|
||||
serialexecution: executionId
|
||||
};
|
||||
|
||||
// D. Dokument in DB anlegen (Drizzle Insert)
|
||||
const [createdDoc] = await server.db
|
||||
.insert(schema.createddocuments)
|
||||
.values(payloadWithRelation)
|
||||
.returning(); // Wichtig für Postgres: returning() gibt das erstellte Objekt zurück
|
||||
|
||||
return createdDoc.id;
|
||||
}
|
||||
|
||||
// --- Drizzle Helper ---
|
||||
|
||||
async function fetchById(server: FastifyInstance, table: any, id: number | null) {
|
||||
if (!id) return null;
|
||||
const [result] = await server.db.select().from(table).where(eq(table.id, id)).limit(1);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
async function getFolderId(server:FastifyInstance, tenantId: number) {
|
||||
const [folder] = await server.db
|
||||
.select({ id: schema.folders.id })
|
||||
.from(schema.folders)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.folders.tenant, tenantId),
|
||||
eq(schema.folders.function, "invoices"), // oder 'invoices', check deine DB
|
||||
eq(schema.folders.year, dayjs().format("YYYY"))
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return folder?.id;
|
||||
}
|
||||
|
||||
async function getFileTypeId(server: FastifyInstance,tenantId: number) {
|
||||
const [tag] = await server.db
|
||||
.select({ id: schema.filetags.id })
|
||||
.from(schema.filetags)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.filetags.tenant, tenantId),
|
||||
eq(schema.filetags.createdDocumentType, "invoices")
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return tag?.id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Logik Helper (Unverändert zur Business Logik) ---
|
||||
|
||||
function calculateDateRange(config: any, executionDate: dayjs.Dayjs) {
|
||||
// Basis nehmen
|
||||
let baseDate = executionDate;
|
||||
|
||||
let firstDate = baseDate;
|
||||
let lastDate = baseDate;
|
||||
|
||||
if (config.intervall === "monatlich" && config.dateDirection === "Rückwirkend") {
|
||||
// 1. Monat abziehen
|
||||
// 2. Start/Ende des Monats berechnen
|
||||
// 3. WICHTIG: Zeit hart auf 12:00:00 setzen, damit Zeitzonen das Datum nicht kippen
|
||||
firstDate = baseDate.subtract(1, "month").startOf("month").hour(12).minute(0).second(0).millisecond(0);
|
||||
lastDate = baseDate.subtract(1, "month").endOf("month").hour(12).minute(0).second(0).millisecond(0);
|
||||
|
||||
} else if (config.intervall === "vierteljährlich" && config.dateDirection === "Rückwirkend") {
|
||||
|
||||
firstDate = baseDate.subtract(1, "quarter").startOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
||||
lastDate = baseDate.subtract(1, "quarter").endOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
||||
}
|
||||
|
||||
// Das Ergebnis ist nun z.B.:
|
||||
// firstDate: '2025-12-01T12:00:00.000Z' (Eindeutig der 1. Dezember)
|
||||
// lastDate: '2025-12-31T12:00:00.000Z' (Eindeutig der 31. Dezember)
|
||||
|
||||
return {
|
||||
firstDate: firstDate.toISOString(),
|
||||
lastDate: lastDate.toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
async function getSaveData(item: any, tenant: any, firstDate: string, lastDate: string, executionDate: string, executedBy: string) {
|
||||
const cleanRows = item.rows.map((row: any) => ({
|
||||
...row,
|
||||
descriptionText: row.description || null,
|
||||
}));
|
||||
|
||||
//const documentNumber = await this.useNextInvoicesNumber(item.tenant);
|
||||
|
||||
return {
|
||||
tenant: item.tenant,
|
||||
type: "invoices",
|
||||
state: "Entwurf",
|
||||
customer: item.customer,
|
||||
contact: item.contact,
|
||||
contract: item.contract,
|
||||
address: item.address,
|
||||
project: item.project,
|
||||
documentDate: executionDate,
|
||||
deliveryDate: firstDate,
|
||||
deliveryDateEnd: lastDate,
|
||||
paymentDays: item.paymentDays,
|
||||
payment_type: item.payment_type,
|
||||
deliveryDateType: item.deliveryDateType,
|
||||
info: {}, // Achtung: Postgres erwartet hier valides JSON Objekt
|
||||
createdBy: item.createdBy,
|
||||
created_by: item.created_by,
|
||||
title: `Rechnung-Nr. XXX`,
|
||||
description: item.description,
|
||||
startText: item.startText,
|
||||
endText: item.endText,
|
||||
rows: cleanRows, // JSON Array
|
||||
contactPerson: item.contactPerson,
|
||||
linkedDocument: item.linkedDocument,
|
||||
letterhead: item.letterhead,
|
||||
taxType: item.taxType,
|
||||
};
|
||||
}
|
||||
|
||||
async function getCloseData(server:FastifyInstance,item: any, tenant: any, units, products,services) {
|
||||
const documentNumber = await useNextNumberRangeNumber(server,tenant.id,"invoices");
|
||||
|
||||
console.log(item);
|
||||
|
||||
const [contact, customer, project, contract] = await Promise.all([
|
||||
fetchById(server, schema.contacts, item.contact),
|
||||
fetchById(server, schema.customers, item.customer),
|
||||
fetchById(server, schema.projects, item.project),
|
||||
fetchById(server, schema.contracts, item.contract),
|
||||
item.letterhead ? fetchById(server, schema.letterheads, item.letterhead) : null
|
||||
|
||||
]);
|
||||
|
||||
const profile = (await server.db.select().from(schema.authProfiles).where(and(eq(schema.authProfiles.user_id, item.created_by),eq(schema.authProfiles.tenant_id,tenant.id))).limit(1))[0];
|
||||
|
||||
console.log(profile)
|
||||
|
||||
const pdfData = getDocumentDataBackend(
|
||||
{
|
||||
...item,
|
||||
state: "Gebucht",
|
||||
documentNumber: documentNumber.usedNumber,
|
||||
title: `Rechnung-Nr. ${documentNumber.usedNumber}`,
|
||||
}, // Das Dokument (mit neuer Nummer)
|
||||
tenant, // Tenant Object
|
||||
customer, // Customer Object
|
||||
contact, // Contact Object (kann null sein)
|
||||
profile, // User Profile (Contact Person)
|
||||
project, // Project Object
|
||||
contract, // Contract Object
|
||||
units, // Units Array
|
||||
products, // Products Array
|
||||
services // Services Array
|
||||
);
|
||||
|
||||
|
||||
return pdfData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Formatiert Zahlen zu deutscher Währung
|
||||
function renderCurrency(value: any, currency = "€") {
|
||||
if (value === undefined || value === null) return "0,00 " + currency;
|
||||
return Number(value).toFixed(2).replace(".", ",") + " " + currency;
|
||||
}
|
||||
|
||||
// Berechnet den Zeilenpreis (Menge * Preis * Rabatt)
|
||||
function getRowAmount(row: any) {
|
||||
const price = Number(row.price || 0);
|
||||
const quantity = Number(row.quantity || 0);
|
||||
const discount = Number(row.discountPercent || 0);
|
||||
return quantity * price * (1 - discount / 100);
|
||||
}
|
||||
|
||||
// Berechnet alle Summen (Netto, Brutto, Steuern, Titel-Summen)
|
||||
// Dies ersetzt 'documentTotal.value' aus dem Frontend
|
||||
function calculateDocumentTotals(rows: any[], taxType: string) {
|
||||
console.log(rows);
|
||||
|
||||
let totalNet = 0;
|
||||
let totalNet19 = 0;
|
||||
let totalNet7 = 0;
|
||||
let totalNet0 = 0;
|
||||
let titleSums: Record<string, number> = {};
|
||||
|
||||
// Aktueller Titel für Gruppierung
|
||||
let currentTitle = "";
|
||||
|
||||
rows.forEach(row => {
|
||||
if (row.mode === 'title') {
|
||||
currentTitle = row.text || row.description || "Titel";
|
||||
if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (['normal', 'service', 'free'].includes(row.mode)) {
|
||||
const amount = getRowAmount(row);
|
||||
totalNet += amount;
|
||||
|
||||
// Summen pro Titel addieren
|
||||
//if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
||||
if(currentTitle.length > 0) titleSums[currentTitle] += amount;
|
||||
|
||||
// Steuer-Logik
|
||||
const tax = taxType === "19 UStG" || taxType === "13b UStG" ? 0 : Number(row.taxPercent);
|
||||
|
||||
if (tax === 19) totalNet19 += amount;
|
||||
else if (tax === 7) totalNet7 += amount;
|
||||
else totalNet0 += amount;
|
||||
}
|
||||
});
|
||||
|
||||
const isTaxFree = ["13b UStG", "19 UStG"].includes(taxType);
|
||||
|
||||
const tax19 = isTaxFree ? 0 : totalNet19 * 0.19;
|
||||
const tax7 = isTaxFree ? 0 : totalNet7 * 0.07;
|
||||
const totalGross = totalNet + tax19 + tax7;
|
||||
|
||||
return {
|
||||
totalNet,
|
||||
totalNet19,
|
||||
totalNet7,
|
||||
totalNet0,
|
||||
total19: tax19,
|
||||
total7: tax7,
|
||||
total0: 0,
|
||||
totalGross,
|
||||
titleSums // Gibt ein Objekt zurück: { "Titel A": 150.00, "Titel B": 200.00 }
|
||||
};
|
||||
}
|
||||
|
||||
export function getDocumentDataBackend(
|
||||
itemInfo: any, // Das Dokument objekt (createddocument)
|
||||
tenant: any, // Tenant Infos (auth.activeTenantData)
|
||||
customerData: any, // Geladener Kunde
|
||||
contactData: any, // Geladener Kontakt (optional)
|
||||
contactPerson: any, // Geladenes User-Profil (ersetzt den API Call)
|
||||
projectData: any, // Projekt
|
||||
contractData: any, // Vertrag
|
||||
units: any[], // Array aller Einheiten
|
||||
products: any[], // Array aller Produkte
|
||||
services: any[] // Array aller Services
|
||||
) {
|
||||
const businessInfo = tenant.businessInfo || {}; // Fallback falls leer
|
||||
|
||||
// --- 1. Agriculture Logic ---
|
||||
// Prüfen ob 'extraModules' existiert, sonst leeres Array annehmen
|
||||
const modules = tenant.extraModules || [];
|
||||
if (modules.includes("agriculture")) {
|
||||
itemInfo.rows.forEach((row: any) => {
|
||||
if (row.agriculture && row.agriculture.dieselUsage) {
|
||||
row.agriculture.description = `${row.agriculture.dieselUsage} L Diesel zu ${renderCurrency(row.agriculture.dieselPrice)}/L verbraucht ${row.description ? "\n" + row.description : ""}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- 2. Tax Override Logic ---
|
||||
let rows = JSON.parse(JSON.stringify(itemInfo.rows)); // Deep Clone um Original nicht zu mutieren
|
||||
if (itemInfo.taxType === "13b UStG" || itemInfo.taxType === "19 UStG") {
|
||||
rows = rows.map((row: any) => ({ ...row, taxPercent: 0 }));
|
||||
}
|
||||
|
||||
// --- 4. Berechnungen (Ersetzt Vue computed props) ---
|
||||
const totals = calculateDocumentTotals(rows, itemInfo.taxType);
|
||||
|
||||
console.log(totals);
|
||||
|
||||
// --- 3. Rows Mapping & Processing ---
|
||||
rows = rows.map((row: any) => {
|
||||
const unit = units.find(i => i.id === row.unit) || { short: "" };
|
||||
|
||||
// Description Text Logic
|
||||
if (!['pagebreak', 'title'].includes(row.mode)) {
|
||||
if (row.agriculture && row.agriculture.description) {
|
||||
row.descriptionText = row.agriculture.description;
|
||||
} else if (row.description) {
|
||||
row.descriptionText = row.description;
|
||||
} else {
|
||||
delete row.descriptionText;
|
||||
}
|
||||
}
|
||||
|
||||
// Product/Service Name Resolution
|
||||
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
||||
if (row.mode === 'normal') {
|
||||
const prod = products.find(i => i.id === row.product);
|
||||
if (prod) row.text = prod.name;
|
||||
}
|
||||
if (row.mode === 'service') {
|
||||
const serv = services.find(i => i.id === row.service);
|
||||
if (serv) row.text = serv.name;
|
||||
}
|
||||
|
||||
const rowAmount = getRowAmount(row);
|
||||
|
||||
return {
|
||||
...row,
|
||||
rowAmount: renderCurrency(rowAmount),
|
||||
quantity: String(row.quantity).replace(".", ","),
|
||||
unit: unit.short,
|
||||
pos: String(row.pos),
|
||||
price: renderCurrency(row.price),
|
||||
discountText: row.discountPercent > 0 ? `(Rabatt: ${row.discountPercent} %)` : ""
|
||||
};
|
||||
} else {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// --- 5. Handlebars Context ---
|
||||
const generateContext = () => {
|
||||
return {
|
||||
// lohnkosten: null, // Backend hat diesen Wert oft nicht direkt, ggf. aus itemInfo holen
|
||||
anrede: (contactData && contactData.salutation) || (customerData && customerData.salutation),
|
||||
titel: (contactData && contactData.title) || (customerData && customerData.title),
|
||||
vorname: (contactData && contactData.firstName) || (customerData && customerData.firstname), // Achte auf casing (firstName vs firstname) in deiner DB
|
||||
nachname: (contactData && contactData.lastName) || (customerData && customerData.lastname),
|
||||
kundenname: customerData && customerData.name,
|
||||
zahlungsziel_in_tagen: itemInfo.paymentDays,
|
||||
zahlungsart: itemInfo.payment_type === "transfer" ? "Überweisung" : "Lastschrift",
|
||||
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) || null
|
||||
};
|
||||
};
|
||||
|
||||
const templateStartText = Handlebars.compile(itemInfo.startText || "");
|
||||
const templateEndText = Handlebars.compile(itemInfo.endText || "");
|
||||
|
||||
// --- 6. Title Sums Formatting ---
|
||||
let returnTitleSums: Record<string, string> = {};
|
||||
Object.keys(totals.titleSums).forEach(key => {
|
||||
returnTitleSums[key] = renderCurrency(totals.titleSums[key]);
|
||||
});
|
||||
|
||||
// Transfer logic (Falls nötig, hier vereinfacht)
|
||||
let returnTitleSumsTransfer = { ...returnTitleSums };
|
||||
|
||||
// --- 7. Construct Final Object ---
|
||||
|
||||
// Adresse aufbereiten
|
||||
const recipientArray = [
|
||||
customerData.name,
|
||||
...(customerData.nameAddition ? [customerData.nameAddition] : []),
|
||||
...(contactData ? [`${contactData.firstName} ${contactData.lastName}`] : []),
|
||||
itemInfo.address?.street || customerData.street || "",
|
||||
...(itemInfo.address?.special ? [itemInfo.address.special] : []),
|
||||
`${itemInfo.address?.zip || customerData.zip} ${itemInfo.address?.city || customerData.city}`,
|
||||
].filter(Boolean); // Leere Einträge entfernen
|
||||
|
||||
console.log(contactPerson)
|
||||
|
||||
// Info Block aufbereiten
|
||||
const infoBlock = [
|
||||
{
|
||||
label: itemInfo.documentNumberTitle || "Rechnungsnummer",
|
||||
content: itemInfo.documentNumber || "ENTWURF",
|
||||
}, {
|
||||
label: "Kundennummer",
|
||||
content: customerData.customerNumber,
|
||||
}, {
|
||||
label: "Belegdatum",
|
||||
content: itemInfo.documentDate ? dayjs(itemInfo.documentDate).format("DD.MM.YYYY") : dayjs().format("DD.MM.YYYY"),
|
||||
},
|
||||
// Lieferdatum Logik
|
||||
...(itemInfo.deliveryDateType !== "Kein Lieferdatum anzeigen" ? [{
|
||||
label: itemInfo.deliveryDateType || "Lieferdatum",
|
||||
content: !['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType)
|
||||
? (itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : "")
|
||||
: `${itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : ""} - ${itemInfo.deliveryDateEnd ? dayjs(itemInfo.deliveryDateEnd).format("DD.MM.YYYY") : ""}`,
|
||||
}] : []),
|
||||
{
|
||||
label: "Ansprechpartner",
|
||||
content: contactPerson ? (contactPerson.name || contactPerson.full_name || contactPerson.email) : "-",
|
||||
},
|
||||
// Kontakt Infos
|
||||
...((itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel) ? [{
|
||||
label: "Telefon",
|
||||
content: itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel,
|
||||
}] : []),
|
||||
...(contactPerson?.email ? [{
|
||||
label: "E-Mail",
|
||||
content: contactPerson.email,
|
||||
}] : []),
|
||||
// Objekt / Projekt / Vertrag
|
||||
...(itemInfo.plant ? [{ label: "Objekt", content: "Objekt Name" }] : []), // Hier müsstest du Plant Data übergeben wenn nötig
|
||||
...(projectData ? [{ label: "Projekt", content: projectData.name }] : []),
|
||||
...(contractData ? [{ label: "Vertrag", content: contractData.contractNumber }] : [])
|
||||
];
|
||||
|
||||
// Total Array für PDF Footer
|
||||
const totalArray = [
|
||||
{
|
||||
label: "Nettobetrag",
|
||||
content: renderCurrency(totals.totalNet),
|
||||
},
|
||||
...(totals.totalNet19 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
||||
label: `zzgl. 19% USt auf ${renderCurrency(totals.totalNet19)}`,
|
||||
content: renderCurrency(totals.total19),
|
||||
}] : []),
|
||||
...(totals.totalNet7 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
||||
label: `zzgl. 7% USt auf ${renderCurrency(totals.totalNet7)}`,
|
||||
content: renderCurrency(totals.total7),
|
||||
}] : []),
|
||||
{
|
||||
label: "Gesamtbetrag",
|
||||
content: renderCurrency(totals.totalGross),
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
...itemInfo,
|
||||
type: itemInfo.type,
|
||||
taxType: itemInfo.taxType,
|
||||
adressLine: `${businessInfo.name || ''}, ${businessInfo.street || ''}, ${businessInfo.zip || ''} ${businessInfo.city || ''}`,
|
||||
recipient: recipientArray,
|
||||
info: infoBlock,
|
||||
title: itemInfo.title,
|
||||
description: itemInfo.description,
|
||||
// Handlebars Compilation ausführen
|
||||
endText: templateEndText(generateContext()),
|
||||
startText: templateStartText(generateContext()),
|
||||
rows: rows,
|
||||
totalArray: totalArray,
|
||||
total: {
|
||||
totalNet: renderCurrency(totals.totalNet),
|
||||
total19: renderCurrency(totals.total19),
|
||||
total0: renderCurrency(totals.total0), // 0% USt Zeilen
|
||||
totalGross: renderCurrency(totals.totalGross),
|
||||
// Diese Werte existieren im einfachen Backend-Kontext oft nicht (Zahlungen checken), daher 0 oder Logik bauen
|
||||
totalGrossAlreadyPaid: renderCurrency(0),
|
||||
totalSumToPay: renderCurrency(totals.totalGross),
|
||||
titleSums: returnTitleSums,
|
||||
titleSumsTransfer: returnTitleSumsTransfer
|
||||
},
|
||||
agriculture: itemInfo.agriculture,
|
||||
// Falls du AdvanceInvoices brauchst, musst du die Objekte hier übergeben oder leer lassen
|
||||
usedAdvanceInvoices: []
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user