diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..126419d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +# Keep environment variables out of version control +.env + +/src/generated/prisma diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..499db90 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..015f7ec --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..da98877 --- /dev/null +++ b/TODO.md @@ -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`) + +--- \ No newline at end of file diff --git a/db/index.ts b/db/index.ts new file mode 100644 index 0000000..eeb9f5b --- /dev/null +++ b/db/index.ts @@ -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) \ No newline at end of file diff --git a/db/migrations/0000_brief_dark_beast.sql b/db/migrations/0000_brief_dark_beast.sql new file mode 100644 index 0000000..adfe24f --- /dev/null +++ b/db/migrations/0000_brief_dark_beast.sql @@ -0,0 +1,1312 @@ +CREATE TYPE "public"."credential_types" AS ENUM('mail', 'm365');--> statement-breakpoint +CREATE TYPE "public"."folderfunctions" AS ENUM('none', 'yearSubCategory', 'incomingInvoices', 'invoices', 'quotes', 'confirmationOrders', 'deliveryNotes', 'vehicleData', 'reminders', 'taxData', 'deposit', 'timeEvaluations');--> statement-breakpoint +CREATE TYPE "public"."locked_tenant" AS ENUM('maintenance_tenant', 'maintenance', 'general', 'no_subscription');--> statement-breakpoint +CREATE TYPE "public"."notification_channel" AS ENUM('email', 'inapp', 'sms', 'push', 'webhook');--> statement-breakpoint +CREATE TYPE "public"."notification_severity" AS ENUM('info', 'success', 'warning', 'error');--> statement-breakpoint +CREATE TYPE "public"."notification_status" AS ENUM('queued', 'sent', 'failed', 'read');--> statement-breakpoint +CREATE TYPE "public"."payment_types" AS ENUM('transfer', 'direct_debit');--> statement-breakpoint +CREATE TYPE "public"."texttemplatepositions" AS ENUM('startText', 'endText');--> statement-breakpoint +CREATE TYPE "public"."times_state" AS ENUM('submitted', 'approved', 'draft');--> statement-breakpoint +CREATE TABLE "accounts" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "accounts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "number" text NOT NULL, + "label" text NOT NULL, + "description" text +); +--> statement-breakpoint +CREATE TABLE "auth_profiles" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" uuid, + "tenant_id" bigint NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "first_name" text NOT NULL, + "last_name" text NOT NULL, + "full_name" text GENERATED ALWAYS AS (((first_name || ' ') || last_name)) STORED, + "mobile_tel" text, + "fixed_tel" text, + "salutation" text, + "employee_number" text, + "weekly_working_hours" double precision DEFAULT 0, + "annual_paid_leave_days" bigint, + "weekly_regular_working_hours" jsonb DEFAULT '{}', + "clothing_size_top" text, + "clothing_size_bottom" text, + "clothing_size_shoe" text, + "email_signature" text DEFAULT '

Mit freundlichen Grüßen

', + "birthday" date, + "entry_date" date DEFAULT now(), + "automatic_hour_corrections" jsonb DEFAULT '[]', + "recreation_days_compensation" boolean DEFAULT true NOT NULL, + "customer_for_portal" bigint, + "pinned_on_navigation" jsonb DEFAULT '[]' NOT NULL, + "email" text, + "token_id" text, + "weekly_working_days" double precision, + "old_profile_id" uuid, + "temp_config" jsonb, + "state_code" text DEFAULT 'DE-NI', + "contract_type" text, + "position" text, + "qualification" text, + "address_street" text, + "address_zip" text, + "address_city" text, + "active" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "auth_role_permissions" ( + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "role_id" uuid NOT NULL, + "permission" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "auth_roles" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "description" text, + "created_by" uuid, + "tenant_id" bigint +); +--> statement-breakpoint +CREATE TABLE "auth_tenant_users" ( + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant_id" bigint NOT NULL, + "user_id" uuid NOT NULL, + "created_by" uuid +); +--> statement-breakpoint +CREATE TABLE "auth_user_roles" ( + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "user_id" uuid NOT NULL, + "role_id" uuid NOT NULL, + "tenant_id" bigint NOT NULL, + "created_by" uuid +); +--> statement-breakpoint +CREATE TABLE "auth_users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "email" text NOT NULL, + "password_hash" text NOT NULL, + "multi_tenant" boolean DEFAULT true NOT NULL, + "must_change_password" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "ported" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "bankaccounts" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "bankaccounts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text, + "iban" text NOT NULL, + "tenant" bigint NOT NULL, + "bankId" text NOT NULL, + "ownerName" text, + "accountId" text NOT NULL, + "balance" double precision, + "expired" boolean DEFAULT false NOT NULL, + "datevNumber" text, + "synced_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "bankrequisitions" ( + "id" uuid PRIMARY KEY NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "institutionId" text, + "tenant" bigint, + "status" text, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "bankstatements" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "bankstatements_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "account" bigint NOT NULL, + "date" text NOT NULL, + "credIban" text, + "credName" text, + "text" text, + "amount" double precision NOT NULL, + "tenant" bigint NOT NULL, + "debIban" text, + "debName" text, + "gocardlessId" text, + "currency" text, + "valueDate" text, + "incomingInvoice" bigint, + "mandateId" text, + "contract" bigint, + "createdDocument" bigint, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "checkexecutions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "check" uuid, + "executed_at" timestamp, + "description" text +); +--> statement-breakpoint +CREATE TABLE "checks" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "vehicle" bigint, + "inventoryitem" bigint, + "tenant" bigint, + "name" text, + "type" text, + "distance" bigint DEFAULT 1, + "distanceUnit" text DEFAULT 'days', + "description" text, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "citys" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "citys_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "name" text, + "short" text, + "long" text, + "geometry" jsonb, + "zip" bigint, + "districtCode" bigint, + "countryName" text, + "countryCode" bigint, + "districtName" text, + "geopoint" text +); +--> statement-breakpoint +CREATE TABLE "contacts" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "contacts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "firstName" text, + "lastName" text, + "email" text, + "customer" bigint, + "tenant" bigint NOT NULL, + "phoneMobile" text, + "phoneHome" text, + "heroId" text, + "role" text, + "fullName" text, + "salutation" text, + "vendor" bigint, + "active" boolean DEFAULT true NOT NULL, + "birthday" date, + "notes" text, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "title" text, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "contracts" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "contracts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "customer" bigint NOT NULL, + "notes" text, + "active" boolean DEFAULT true NOT NULL, + "recurring" boolean DEFAULT false NOT NULL, + "rhythm" jsonb, + "startDate" timestamp with time zone, + "endDate" timestamp with time zone, + "signDate" timestamp with time zone, + "duration" text, + "contact" bigint, + "bankingIban" text, + "bankingBIC" text, + "bankingName" text, + "bankingOwner" text, + "sepaRef" text, + "sepaDate" timestamp with time zone, + "paymentType" text, + "invoiceDispatch" text, + "ownFields" jsonb DEFAULT '{}'::jsonb NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "contractNumber" text, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "costcentres" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "number" text NOT NULL, + "name" text NOT NULL, + "vehicle" bigint, + "project" bigint, + "inventoryitem" bigint, + "description" text, + "archived" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "countrys" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "countrys_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "createddocuments" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "createddocuments_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "type" text DEFAULT 'INVOICE' NOT NULL, + "customer" bigint, + "contact" bigint, + "address" jsonb, + "project" bigint, + "documentNumber" text, + "documentDate" text, + "state" text DEFAULT 'Entwurf' NOT NULL, + "info" jsonb, + "createdBy" uuid, + "title" text, + "description" text, + "startText" text, + "endText" text, + "rows" jsonb DEFAULT '[]'::jsonb, + "deliveryDateType" text, + "paymentDays" smallint, + "deliveryDate" text, + "contactPerson" uuid, + "serialConfig" jsonb DEFAULT '{}'::jsonb, + "linkedDocument" bigint, + "agriculture" jsonb, + "letterhead" bigint, + "advanceInvoiceResolved" boolean DEFAULT false NOT NULL, + "usedAdvanceInvoices" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "deliveryDateEnd" text, + "plant" bigint, + "taxType" text, + "customSurchargePercentage" smallint DEFAULT 0 NOT NULL, + "report" jsonb DEFAULT '{}'::jsonb NOT NULL, + "availableInPortal" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "created_by" uuid, + "payment_type" text DEFAULT 'transfer', + "contract" bigint +); +--> statement-breakpoint +CREATE TABLE "createdletters" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint, + "customer" bigint, + "vendor" bigint, + "content_json" jsonb DEFAULT '[]'::jsonb, + "content_text" text, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "customers" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customers_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "customerNumber" text NOT NULL, + "name" text NOT NULL, + "tenant" bigint NOT NULL, + "infoData" jsonb DEFAULT '{}'::jsonb, + "active" boolean DEFAULT true NOT NULL, + "notes" text, + "type" text DEFAULT 'Privat', + "heroId" text, + "isCompany" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "customPaymentDays" smallint, + "firstname" text, + "lastname" text, + "archived" boolean DEFAULT false NOT NULL, + "customSurchargePercentage" smallint DEFAULT 0 NOT NULL, + "salutation" text, + "title" text, + "nameAddition" text, + "availableInPortal" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "custom_payment_type" text +); +--> statement-breakpoint +CREATE TABLE "devices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "type" text NOT NULL, + "tenant" bigint, + "password" text, + "externalId" text +); +--> statement-breakpoint +CREATE TABLE "documentboxes" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "space" bigint, + "key" text NOT NULL, + "tenant" bigint NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "events" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "events_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "startDate" timestamp with time zone NOT NULL, + "endDate" timestamp with time zone, + "eventtype" text DEFAULT 'Umsetzung', + "project" bigint, + "resources" jsonb DEFAULT '[]'::jsonb, + "notes" text, + "link" text, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "vehicles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "inventoryitems" jsonb DEFAULT '[]'::jsonb NOT NULL, + "inventoryitemgroups" jsonb DEFAULT '[]'::jsonb NOT NULL, + "customer" bigint, + "vendor" bigint, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "files" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "path" text, + "tenant" bigint NOT NULL, + "project" bigint, + "customer" bigint, + "contract" bigint, + "vendor" bigint, + "incominginvoice" bigint, + "plant" bigint, + "createddocument" bigint, + "vehicle" bigint, + "product" bigint, + "check" uuid, + "inventoryitem" bigint, + "folder" uuid, + "mimeType" text, + "archived" boolean DEFAULT false NOT NULL, + "space" bigint, + "type" uuid, + "documentbox" uuid, + "name" text, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "created_by" uuid, + "auth_profile" uuid +); +--> statement-breakpoint +CREATE TABLE "filetags" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "color" text, + "tenant" bigint NOT NULL, + "createddocumenttype" text DEFAULT '', + "incomingDocumentType" text, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "folders" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "icon" text, + "parent" uuid, + "isSystemUsed" boolean DEFAULT false NOT NULL, + "function" "folderfunctions", + "year" integer, + "standardFiletype" uuid, + "standardFiletypeIsOptional" boolean DEFAULT true NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "exports" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "exports_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant_id" bigint NOT NULL, + "start_date" timestamp with time zone NOT NULL, + "end_date" timestamp with time zone NOT NULL, + "valid_until" timestamp with time zone, + "type" text DEFAULT 'datev' NOT NULL, + "url" text NOT NULL, + "file_path" text +); +--> statement-breakpoint +CREATE TABLE "globalmessages" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "globalmessages_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "title" text, + "description" text +); +--> statement-breakpoint +CREATE TABLE "globalmessagesseen" ( + "message" bigint NOT NULL, + "seen_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "helpdesk_channel_instances" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "type_id" text NOT NULL, + "name" text NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "config" jsonb NOT NULL, + "public_config" jsonb DEFAULT '{}'::jsonb NOT NULL, + "public_token" text, + "secret_token" text, + "created_by" uuid, + "created_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "helpdesk_channel_instances_public_token_unique" UNIQUE("public_token") +); +--> statement-breakpoint +CREATE TABLE "helpdesk_channel_types" ( + "id" text PRIMARY KEY NOT NULL, + "description" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "helpdesk_contacts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "customer_id" bigint, + "email" text, + "phone" text, + "external_ref" jsonb, + "display_name" text, + "created_at" timestamp with time zone DEFAULT now(), + "source_channel_id" uuid, + "contact_id" bigint +); +--> statement-breakpoint +CREATE TABLE "helpdesk_conversation_participants" ( + "conversation_id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "role" text +); +--> statement-breakpoint +CREATE TABLE "helpdesk_conversations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "channel_instance_id" uuid NOT NULL, + "contact_id" uuid, + "subject" text, + "status" text DEFAULT 'open' NOT NULL, + "priority" text DEFAULT 'normal', + "assignee_user_id" uuid, + "last_message_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now(), + "customer_id" bigint, + "contact_person_id" bigint, + "ticket_number" text +); +--> statement-breakpoint +CREATE TABLE "helpdesk_messages" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "conversation_id" uuid NOT NULL, + "direction" text NOT NULL, + "author_user_id" uuid, + "payload" jsonb NOT NULL, + "raw_meta" jsonb, + "created_at" timestamp with time zone DEFAULT now(), + "contact_id" uuid, + "external_message_id" text, + "received_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "helpdesk_messages_external_message_id_unique" UNIQUE("external_message_id") +); +--> statement-breakpoint +CREATE TABLE "helpdesk_routing_rules" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "name" text NOT NULL, + "condition" jsonb NOT NULL, + "action" jsonb NOT NULL, + "created_by" uuid, + "created_at" timestamp with time zone DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "historyitems" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "historyitems_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "text" text NOT NULL, + "customer" bigint, + "tenant" bigint NOT NULL, + "vendor" bigint, + "project" bigint, + "plant" bigint, + "incomingInvoice" bigint, + "contact" bigint, + "inventoryitem" bigint, + "product" bigint, + "event" bigint, + "newVal" text, + "oldVal" text, + "task" bigint, + "vehicle" bigint, + "bankstatement" bigint, + "space" bigint, + "config" jsonb, + "projecttype" bigint, + "check" uuid, + "service" bigint, + "createddocument" bigint, + "file" uuid, + "inventoryitemgroup" uuid, + "source" text DEFAULT 'Software', + "costcentre" uuid, + "ownaccount" uuid, + "documentbox" uuid, + "hourrate" uuid, + "created_by" uuid, + "action" text +); +--> statement-breakpoint +CREATE TABLE "holidays" ( + "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "holidays_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "date" date NOT NULL, + "name" text NOT NULL, + "state_code" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "hourrates" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "purchasePrice" double precision NOT NULL, + "sellingPrice" double precision NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "incominginvoices" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "incominginvoices_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "state" text DEFAULT 'Entwurf' NOT NULL, + "vendor" bigint, + "reference" text, + "date" text, + "document" bigint, + "dueDate" text, + "description" text, + "paymentType" text, + "accounts" jsonb DEFAULT '[{"account":null,"taxType":null,"amountNet":null,"amountTax":19,"costCentre":null}]'::jsonb NOT NULL, + "paid" boolean DEFAULT false NOT NULL, + "expense" boolean DEFAULT true NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "inventoryitemgroups" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "inventoryitems" jsonb DEFAULT '[]'::jsonb NOT NULL, + "description" text, + "archived" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "usePlanning" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "inventoryitems" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "inventoryitems_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "usePlanning" boolean DEFAULT false NOT NULL, + "description" text, + "tenant" bigint NOT NULL, + "currentSpace" bigint, + "articleNumber" text, + "serialNumber" text, + "purchaseDate" date, + "vendor" bigint, + "quantity" bigint DEFAULT 0 NOT NULL, + "purchasePrice" double precision DEFAULT 0, + "manufacturer" text, + "manufacturerNumber" text, + "currentValue" double precision, + "archived" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "letterheads" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "letterheads_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text DEFAULT 'Standard', + "path" text NOT NULL, + "documentTypes" text[] DEFAULT '{}' NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "movements" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "movements_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "quantity" bigint NOT NULL, + "productId" bigint NOT NULL, + "spaceId" bigint, + "tenant" bigint NOT NULL, + "projectId" bigint, + "notes" text, + "serials" text[], + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "notifications_event_types" ( + "event_key" text PRIMARY KEY NOT NULL, + "display_name" text NOT NULL, + "description" text, + "category" text, + "severity" "notification_severity" DEFAULT 'info' NOT NULL, + "allowed_channels" jsonb DEFAULT '["inapp","email"]'::jsonb NOT NULL, + "payload_schema" jsonb, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notifications_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "user_id" uuid NOT NULL, + "event_type" text NOT NULL, + "title" text NOT NULL, + "message" text NOT NULL, + "payload" jsonb, + "channel" "notification_channel" NOT NULL, + "status" "notification_status" DEFAULT 'queued' NOT NULL, + "error" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "sent_at" timestamp with time zone, + "read_at" timestamp with time zone +); +--> statement-breakpoint +CREATE TABLE "notifications_preferences" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "user_id" uuid NOT NULL, + "event_type" text NOT NULL, + "channel" "notification_channel" NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notifications_preferences_defaults" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "event_key" text NOT NULL, + "channel" "notification_channel" NOT NULL, + "enabled" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "ownaccounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "number" text NOT NULL, + "name" text NOT NULL, + "description" text, + "archived" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "plants" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "plants_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "customer" bigint, + "infoData" jsonb, + "contract" bigint, + "description" jsonb DEFAULT '{"html":"","json":[],"text":""}'::jsonb, + "archived" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "productcategories" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "productcategories_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "description" text, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "products" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "products_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "manufacturer" text, + "unit" bigint NOT NULL, + "tags" json DEFAULT '[]'::json NOT NULL, + "tenant" bigint NOT NULL, + "ean" text, + "barcode" text, + "purchasePrice" double precision, + "sellingPrice" double precision, + "description" text, + "manufacturerNumber" text, + "vendorAllocation" jsonb DEFAULT '[]'::jsonb, + "articleNumber" text, + "barcodes" text[] DEFAULT '{}' NOT NULL, + "productcategories" jsonb DEFAULT '[]'::jsonb, + "archived" boolean DEFAULT false NOT NULL, + "taxPercentage" smallint DEFAULT 19 NOT NULL, + "markupPercentage" double precision, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "projects" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "projects_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "notes" text, + "customer" bigint, + "phases" jsonb DEFAULT '[]'::jsonb, + "description" json, + "forms" jsonb DEFAULT '[]'::jsonb, + "heroId" text, + "measure" text, + "material" jsonb, + "plant" bigint, + "profiles" uuid[] DEFAULT '{}' NOT NULL, + "projectNumber" text, + "contract" bigint, + "projectType" text DEFAULT 'Projekt', + "projecttype" bigint, + "archived" boolean DEFAULT false NOT NULL, + "customerRef" text, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "active_phase" text +); +--> statement-breakpoint +CREATE TABLE "projecttypes" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "projecttypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "initialPhases" jsonb, + "addablePhases" jsonb, + "icon" text, + "tenant" bigint NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "servicecategories" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "servicecategories_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "description" text, + "discount" double precision DEFAULT 0, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "services" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "services_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "sellingPrice" double precision, + "description" text, + "tenant" bigint NOT NULL, + "unit" bigint, + "serviceNumber" bigint, + "tags" jsonb DEFAULT '[]'::jsonb, + "servicecategories" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "purchasePriceComposed" jsonb DEFAULT '{"total":0}'::jsonb NOT NULL, + "sellingPriceComposed" jsonb DEFAULT '{"total":0}'::jsonb NOT NULL, + "taxPercentage" smallint DEFAULT 19 NOT NULL, + "materialComposition" jsonb DEFAULT '[]'::jsonb NOT NULL, + "personalComposition" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "spaces" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "spaces_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text, + "type" text NOT NULL, + "tenant" bigint NOT NULL, + "spaceNumber" text NOT NULL, + "parentSpace" bigint, + "infoData" jsonb DEFAULT '{"zip":"","city":"","streetNumber":""}'::jsonb NOT NULL, + "description" text, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "staff_time_entries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "tenant_id" bigint NOT NULL, + "user_id" uuid NOT NULL, + "started_at" timestamp with time zone NOT NULL, + "stopped_at" timestamp with time zone, + "duration_minutes" integer GENERATED ALWAYS AS (CASE + WHEN stopped_at IS NOT NULL + THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60) + ELSE NULL + END) STORED, + "type" text DEFAULT 'work', + "description" text, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + "archived" boolean DEFAULT false NOT NULL, + "updated_by" uuid, + "source" text, + "state" times_state DEFAULT 'draft' NOT NULL, + "device" uuid, + "internal_note" text, + "vacation_reason" text, + "vacation_days" numeric(5, 2), + "approved_by" uuid, + "approved_at" timestamp with time zone, + "sick_reason" text +); +--> statement-breakpoint +CREATE TABLE "staff_time_entry_connects" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "time_entry_id" uuid NOT NULL, + "project_id" bigint, + "started_at" timestamp with time zone NOT NULL, + "stopped_at" timestamp with time zone NOT NULL, + "duration_minutes" integer GENERATED ALWAYS AS ((EXTRACT(epoch FROM (stopped_at - started_at)) / 60)) STORED, + "notes" text, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "staff_zeitstromtimestamps" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "profile" uuid NOT NULL, + "key" text NOT NULL, + "intent" text NOT NULL, + "time" timestamp with time zone NOT NULL, + "staff_time_entry" uuid, + "internal_note" text +); +--> statement-breakpoint +CREATE TABLE "statementallocations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "bs_id" integer NOT NULL, + "cd_id" integer, + "amount" double precision DEFAULT 0 NOT NULL, + "ii_id" bigint, + "tenant" bigint NOT NULL, + "account" bigint, + "created_at" timestamp DEFAULT now(), + "ownaccount" uuid, + "description" text, + "customer" bigint, + "vendor" bigint, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "archived" boolean DEFAULT false NOT NULL +); +--> statement-breakpoint +CREATE TABLE "tasks" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "tasks_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "description" text, + "categorie" text, + "tenant" bigint NOT NULL, + "user_id" uuid, + "project" bigint, + "plant" bigint, + "customer" bigint, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "taxtypes" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "taxtypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "label" text NOT NULL, + "percentage" bigint NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "tenants" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "tenants_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "short" text NOT NULL, + "calendarConfig" jsonb DEFAULT '{"eventTypes":[{"color":"blue","label":"Büro"},{"color":"yellow","label":"Besprechung"},{"color":"green","label":"Umsetzung"},{"color":"red","label":"Vor Ort Termin"}]}'::jsonb, + "timeConfig" jsonb DEFAULT '{}'::jsonb NOT NULL, + "tags" jsonb DEFAULT '{"products":[],"documents":[]}'::jsonb NOT NULL, + "measures" jsonb 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"}]'::jsonb NOT NULL, + "businessInfo" jsonb DEFAULT '{"zip":"","city":"","name":"","street":""}'::jsonb, + "features" jsonb DEFAULT '{"objects":true,"calendar":true,"contacts":true,"projects":true,"vehicles":true,"contracts":true,"inventory":true,"accounting":true,"timeTracking":true,"planningBoard":true,"workingTimeTracking":true}'::jsonb, + "ownFields" jsonb, + "numberRanges" jsonb 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}}'::jsonb NOT NULL, + "standardEmailForInvoices" text, + "extraModules" jsonb DEFAULT '[]'::jsonb NOT NULL, + "isInTrial" boolean DEFAULT false, + "trialEndDate" date, + "stripeCustomerId" text, + "hasActiveLicense" boolean DEFAULT false NOT NULL, + "userLicenseCount" integer DEFAULT 0 NOT NULL, + "workstationLicenseCount" integer DEFAULT 0 NOT NULL, + "standardPaymentDays" smallint DEFAULT 14 NOT NULL, + "dokuboxEmailAddresses" jsonb DEFAULT '[]'::jsonb, + "dokuboxkey" uuid DEFAULT gen_random_uuid() NOT NULL, + "autoPrepareIncomingInvoices" boolean DEFAULT true, + "portalDomain" text, + "portalConfig" jsonb DEFAULT '{"primayColor":"#69c350"}'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid, + "locked" "locked_tenant" +); +--> statement-breakpoint +CREATE TABLE "texttemplates" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "texttemplates_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "name" text NOT NULL, + "text" text NOT NULL, + "documentType" text DEFAULT '', + "default" boolean DEFAULT false NOT NULL, + "pos" texttemplatepositions NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "units" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "units_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "single" text NOT NULL, + "multiple" text, + "short" text, + "step" text DEFAULT '1' NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_credentials" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "user_id" uuid NOT NULL, + "updated_at" timestamp with time zone, + "tenant_id" bigint NOT NULL, + "smtp_port" numeric, + "smtp_ssl" boolean, + "type" "credential_types" NOT NULL, + "imap_port" numeric, + "imap_ssl" boolean, + "email_encrypted" jsonb, + "password_encrypted" jsonb, + "smtp_host_encrypted" jsonb, + "imap_host_encrypted" jsonb, + "access_token_encrypted" jsonb, + "refresh_token_encrypted" jsonb +); +--> statement-breakpoint +CREATE TABLE "vehicles" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "vehicles_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "tenant" bigint NOT NULL, + "licensePlate" text, + "name" text, + "type" text, + "active" boolean DEFAULT true, + "driver" uuid, + "vin" text, + "tankSize" double precision DEFAULT 0 NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "buildYear" text, + "towingCapacity" bigint, + "powerInKW" bigint, + "color" text, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +CREATE TABLE "vendors" ( + "id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "vendors_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "vendorNumber" text NOT NULL, + "tenant" bigint NOT NULL, + "infoData" jsonb DEFAULT '{}'::jsonb NOT NULL, + "notes" text, + "hasSEPA" boolean DEFAULT false NOT NULL, + "profiles" jsonb DEFAULT '[]'::jsonb NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "defaultPaymentMethod" text, + "updated_at" timestamp with time zone, + "updated_by" uuid +); +--> statement-breakpoint +ALTER TABLE "auth_profiles" ADD CONSTRAINT "auth_profiles_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 "auth_role_permissions" ADD CONSTRAINT "auth_role_permissions_role_id_auth_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."auth_roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_roles" ADD CONSTRAINT "auth_roles_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_tenant_users" ADD CONSTRAINT "auth_tenant_users_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_user_roles" ADD CONSTRAINT "auth_user_roles_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 "auth_user_roles" ADD CONSTRAINT "auth_user_roles_role_id_auth_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."auth_roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auth_user_roles" ADD CONSTRAINT "auth_user_roles_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankaccounts" ADD CONSTRAINT "bankaccounts_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankaccounts" ADD CONSTRAINT "bankaccounts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankrequisitions" ADD CONSTRAINT "bankrequisitions_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankrequisitions" ADD CONSTRAINT "bankrequisitions_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_account_bankaccounts_id_fk" FOREIGN KEY ("account") REFERENCES "public"."bankaccounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_incomingInvoice_incominginvoices_id_fk" FOREIGN KEY ("incomingInvoice") REFERENCES "public"."incominginvoices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_createdDocument_createddocuments_id_fk" FOREIGN KEY ("createdDocument") REFERENCES "public"."createddocuments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "bankstatements" ADD CONSTRAINT "bankstatements_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checkexecutions" ADD CONSTRAINT "checkexecutions_check_checks_id_fk" FOREIGN KEY ("check") REFERENCES "public"."checks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checks" ADD CONSTRAINT "checks_vehicle_vehicles_id_fk" FOREIGN KEY ("vehicle") REFERENCES "public"."vehicles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checks" ADD CONSTRAINT "checks_inventoryitem_inventoryitems_id_fk" FOREIGN KEY ("inventoryitem") REFERENCES "public"."inventoryitems"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checks" ADD CONSTRAINT "checks_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "checks" ADD CONSTRAINT "checks_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contacts" ADD CONSTRAINT "contacts_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contacts" ADD CONSTRAINT "contacts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contracts" ADD CONSTRAINT "contracts_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contracts" ADD CONSTRAINT "contracts_contact_contacts_id_fk" FOREIGN KEY ("contact") REFERENCES "public"."contacts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "contracts" ADD CONSTRAINT "contracts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_vehicle_vehicles_id_fk" FOREIGN KEY ("vehicle") REFERENCES "public"."vehicles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_project_projects_id_fk" FOREIGN KEY ("project") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_inventoryitem_inventoryitems_id_fk" FOREIGN KEY ("inventoryitem") REFERENCES "public"."inventoryitems"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_contact_contacts_id_fk" FOREIGN KEY ("contact") REFERENCES "public"."contacts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_project_projects_id_fk" FOREIGN KEY ("project") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_createdBy_auth_users_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_linkedDocument_createddocuments_id_fk" FOREIGN KEY ("linkedDocument") REFERENCES "public"."createddocuments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_letterhead_letterheads_id_fk" FOREIGN KEY ("letterhead") REFERENCES "public"."letterheads"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_plant_plants_id_fk" FOREIGN KEY ("plant") REFERENCES "public"."plants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createdletters" ADD CONSTRAINT "createdletters_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createdletters" ADD CONSTRAINT "createdletters_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createdletters" ADD CONSTRAINT "createdletters_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "createdletters" ADD CONSTRAINT "createdletters_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "customers" ADD CONSTRAINT "customers_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "devices" ADD CONSTRAINT "devices_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "documentboxes" ADD CONSTRAINT "documentboxes_space_spaces_id_fk" FOREIGN KEY ("space") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "documentboxes" ADD CONSTRAINT "documentboxes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "documentboxes" ADD CONSTRAINT "documentboxes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "events" ADD CONSTRAINT "events_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "events" ADD CONSTRAINT "events_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_project_projects_id_fk" FOREIGN KEY ("project") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_incominginvoice_incominginvoices_id_fk" FOREIGN KEY ("incominginvoice") REFERENCES "public"."incominginvoices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_plant_plants_id_fk" FOREIGN KEY ("plant") REFERENCES "public"."plants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_createddocument_createddocuments_id_fk" FOREIGN KEY ("createddocument") REFERENCES "public"."createddocuments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_vehicle_vehicles_id_fk" FOREIGN KEY ("vehicle") REFERENCES "public"."vehicles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_product_products_id_fk" FOREIGN KEY ("product") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_check_checks_id_fk" FOREIGN KEY ("check") REFERENCES "public"."checks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_inventoryitem_inventoryitems_id_fk" FOREIGN KEY ("inventoryitem") REFERENCES "public"."inventoryitems"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_folder_folders_id_fk" FOREIGN KEY ("folder") REFERENCES "public"."folders"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_space_spaces_id_fk" FOREIGN KEY ("space") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_type_filetags_id_fk" FOREIGN KEY ("type") REFERENCES "public"."filetags"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_documentbox_documentboxes_id_fk" FOREIGN KEY ("documentbox") REFERENCES "public"."documentboxes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "files" ADD CONSTRAINT "files_auth_profile_auth_profiles_id_fk" FOREIGN KEY ("auth_profile") REFERENCES "public"."auth_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "filetags" ADD CONSTRAINT "filetags_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "folders" ADD CONSTRAINT "folders_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "folders" ADD CONSTRAINT "folders_parent_folders_id_fk" FOREIGN KEY ("parent") REFERENCES "public"."folders"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "folders" ADD CONSTRAINT "folders_standardFiletype_filetags_id_fk" FOREIGN KEY ("standardFiletype") REFERENCES "public"."filetags"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "folders" ADD CONSTRAINT "folders_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "exports" ADD CONSTRAINT "exports_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 "globalmessagesseen" ADD CONSTRAINT "globalmessagesseen_message_globalmessages_id_fk" FOREIGN KEY ("message") REFERENCES "public"."globalmessages"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_channel_instances" ADD CONSTRAINT "helpdesk_channel_instances_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_channel_instances" ADD CONSTRAINT "helpdesk_channel_instances_type_id_helpdesk_channel_types_id_fk" FOREIGN KEY ("type_id") REFERENCES "public"."helpdesk_channel_types"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_channel_instances" ADD CONSTRAINT "helpdesk_channel_instances_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_contacts" ADD CONSTRAINT "helpdesk_contacts_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_contacts" ADD CONSTRAINT "helpdesk_contacts_customer_id_customers_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_contacts" ADD CONSTRAINT "helpdesk_contacts_source_channel_id_helpdesk_channel_instances_id_fk" FOREIGN KEY ("source_channel_id") REFERENCES "public"."helpdesk_channel_instances"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_contacts" ADD CONSTRAINT "helpdesk_contacts_contact_id_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversation_participants" ADD CONSTRAINT "helpdesk_conversation_participants_conversation_id_helpdesk_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."helpdesk_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversation_participants" ADD CONSTRAINT "helpdesk_conversation_participants_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_channel_instance_id_helpdesk_channel_instances_id_fk" FOREIGN KEY ("channel_instance_id") REFERENCES "public"."helpdesk_channel_instances"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_contact_id_helpdesk_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."helpdesk_contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_assignee_user_id_auth_users_id_fk" FOREIGN KEY ("assignee_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_customer_id_customers_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."customers"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_conversations" ADD CONSTRAINT "helpdesk_conversations_contact_person_id_contacts_id_fk" FOREIGN KEY ("contact_person_id") REFERENCES "public"."contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_messages" ADD CONSTRAINT "helpdesk_messages_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_messages" ADD CONSTRAINT "helpdesk_messages_conversation_id_helpdesk_conversations_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."helpdesk_conversations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_messages" ADD CONSTRAINT "helpdesk_messages_author_user_id_auth_users_id_fk" FOREIGN KEY ("author_user_id") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_messages" ADD CONSTRAINT "helpdesk_messages_contact_id_helpdesk_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."helpdesk_contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_routing_rules" ADD CONSTRAINT "helpdesk_routing_rules_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "helpdesk_routing_rules" ADD CONSTRAINT "helpdesk_routing_rules_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_project_projects_id_fk" FOREIGN KEY ("project") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_plant_plants_id_fk" FOREIGN KEY ("plant") REFERENCES "public"."plants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_incomingInvoice_incominginvoices_id_fk" FOREIGN KEY ("incomingInvoice") REFERENCES "public"."incominginvoices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_contact_contacts_id_fk" FOREIGN KEY ("contact") REFERENCES "public"."contacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_inventoryitem_inventoryitems_id_fk" FOREIGN KEY ("inventoryitem") REFERENCES "public"."inventoryitems"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_product_products_id_fk" FOREIGN KEY ("product") REFERENCES "public"."products"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_event_events_id_fk" FOREIGN KEY ("event") REFERENCES "public"."events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_task_tasks_id_fk" FOREIGN KEY ("task") REFERENCES "public"."tasks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_vehicle_vehicles_id_fk" FOREIGN KEY ("vehicle") REFERENCES "public"."vehicles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_bankstatement_bankstatements_id_fk" FOREIGN KEY ("bankstatement") REFERENCES "public"."bankstatements"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_space_spaces_id_fk" FOREIGN KEY ("space") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_projecttype_projecttypes_id_fk" FOREIGN KEY ("projecttype") REFERENCES "public"."projecttypes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_check_checks_id_fk" FOREIGN KEY ("check") REFERENCES "public"."checks"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_service_services_id_fk" FOREIGN KEY ("service") REFERENCES "public"."services"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_createddocument_createddocuments_id_fk" FOREIGN KEY ("createddocument") REFERENCES "public"."createddocuments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_file_files_id_fk" FOREIGN KEY ("file") REFERENCES "public"."files"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_inventoryitemgroup_inventoryitemgroups_id_fk" FOREIGN KEY ("inventoryitemgroup") REFERENCES "public"."inventoryitemgroups"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_costcentre_costcentres_id_fk" FOREIGN KEY ("costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_ownaccount_ownaccounts_id_fk" FOREIGN KEY ("ownaccount") REFERENCES "public"."ownaccounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_documentbox_documentboxes_id_fk" FOREIGN KEY ("documentbox") REFERENCES "public"."documentboxes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_hourrate_hourrates_id_fk" FOREIGN KEY ("hourrate") REFERENCES "public"."hourrates"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hourrates" ADD CONSTRAINT "hourrates_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hourrates" ADD CONSTRAINT "hourrates_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "incominginvoices" ADD CONSTRAINT "incominginvoices_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "incominginvoices" ADD CONSTRAINT "incominginvoices_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "incominginvoices" ADD CONSTRAINT "incominginvoices_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitemgroups" ADD CONSTRAINT "inventoryitemgroups_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitemgroups" ADD CONSTRAINT "inventoryitemgroups_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitems" ADD CONSTRAINT "inventoryitems_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitems" ADD CONSTRAINT "inventoryitems_currentSpace_spaces_id_fk" FOREIGN KEY ("currentSpace") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitems" ADD CONSTRAINT "inventoryitems_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "inventoryitems" ADD CONSTRAINT "inventoryitems_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "letterheads" ADD CONSTRAINT "letterheads_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "letterheads" ADD CONSTRAINT "letterheads_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "movements" ADD CONSTRAINT "movements_productId_products_id_fk" FOREIGN KEY ("productId") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "movements" ADD CONSTRAINT "movements_spaceId_spaces_id_fk" FOREIGN KEY ("spaceId") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "movements" ADD CONSTRAINT "movements_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "movements" ADD CONSTRAINT "movements_projectId_projects_id_fk" FOREIGN KEY ("projectId") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "movements" ADD CONSTRAINT "movements_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notifications_items" ADD CONSTRAINT "notifications_items_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_items" ADD CONSTRAINT "notifications_items_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_items" ADD CONSTRAINT "notifications_items_event_type_notifications_event_types_event_key_fk" FOREIGN KEY ("event_type") REFERENCES "public"."notifications_event_types"("event_key") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_preferences" ADD CONSTRAINT "notifications_preferences_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_preferences" ADD CONSTRAINT "notifications_preferences_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_preferences" ADD CONSTRAINT "notifications_preferences_event_type_notifications_event_types_event_key_fk" FOREIGN KEY ("event_type") REFERENCES "public"."notifications_event_types"("event_key") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_preferences_defaults" ADD CONSTRAINT "notifications_preferences_defaults_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "notifications_preferences_defaults" ADD CONSTRAINT "notifications_preferences_defaults_event_key_notifications_event_types_event_key_fk" FOREIGN KEY ("event_key") REFERENCES "public"."notifications_event_types"("event_key") ON DELETE restrict ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "ownaccounts" ADD CONSTRAINT "ownaccounts_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "ownaccounts" ADD CONSTRAINT "ownaccounts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "plants" ADD CONSTRAINT "plants_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "plants" ADD CONSTRAINT "plants_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "plants" ADD CONSTRAINT "plants_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "plants" ADD CONSTRAINT "plants_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "productcategories" ADD CONSTRAINT "productcategories_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "productcategories" ADD CONSTRAINT "productcategories_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "products" ADD CONSTRAINT "products_unit_units_id_fk" FOREIGN KEY ("unit") REFERENCES "public"."units"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "products" ADD CONSTRAINT "products_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "products" ADD CONSTRAINT "products_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_projecttype_projecttypes_id_fk" FOREIGN KEY ("projecttype") REFERENCES "public"."projecttypes"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projecttypes" ADD CONSTRAINT "projecttypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projecttypes" ADD CONSTRAINT "projecttypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "servicecategories" ADD CONSTRAINT "servicecategories_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "servicecategories" ADD CONSTRAINT "servicecategories_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "services" ADD CONSTRAINT "services_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "services" ADD CONSTRAINT "services_unit_units_id_fk" FOREIGN KEY ("unit") REFERENCES "public"."units"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "services" ADD CONSTRAINT "services_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "spaces" ADD CONSTRAINT "spaces_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "spaces" ADD CONSTRAINT "spaces_parentSpace_spaces_id_fk" FOREIGN KEY ("parentSpace") REFERENCES "public"."spaces"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "spaces" ADD CONSTRAINT "spaces_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_time_entries" ADD CONSTRAINT "staff_time_entries_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_entries" ADD CONSTRAINT "staff_time_entries_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_time_entries" ADD CONSTRAINT "staff_time_entries_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_time_entries" ADD CONSTRAINT "staff_time_entries_approved_by_auth_users_id_fk" FOREIGN KEY ("approved_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_time_entry_connects" ADD CONSTRAINT "staff_time_entry_connects_time_entry_id_staff_time_entries_id_fk" FOREIGN KEY ("time_entry_id") REFERENCES "public"."staff_time_entries"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_zeitstromtimestamps" ADD CONSTRAINT "staff_zeitstromtimestamps_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_zeitstromtimestamps" ADD CONSTRAINT "staff_zeitstromtimestamps_profile_auth_profiles_id_fk" FOREIGN KEY ("profile") REFERENCES "public"."auth_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "staff_zeitstromtimestamps" ADD CONSTRAINT "staff_zeitstromtimestamps_staff_time_entry_staff_time_entries_id_fk" FOREIGN KEY ("staff_time_entry") REFERENCES "public"."staff_time_entries"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_bs_id_bankstatements_id_fk" FOREIGN KEY ("bs_id") REFERENCES "public"."bankstatements"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_cd_id_createddocuments_id_fk" FOREIGN KEY ("cd_id") REFERENCES "public"."createddocuments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_ii_id_incominginvoices_id_fk" FOREIGN KEY ("ii_id") REFERENCES "public"."incominginvoices"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_account_accounts_id_fk" FOREIGN KEY ("account") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_ownaccount_ownaccounts_id_fk" FOREIGN KEY ("ownaccount") REFERENCES "public"."ownaccounts"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_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 "tasks" ADD CONSTRAINT "tasks_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tasks" ADD CONSTRAINT "tasks_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "taxtypes" ADD CONSTRAINT "taxtypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "tenants" ADD CONSTRAINT "tenants_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "texttemplates" ADD CONSTRAINT "texttemplates_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "texttemplates" ADD CONSTRAINT "texttemplates_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_credentials" ADD CONSTRAINT "user_credentials_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 "user_credentials" ADD CONSTRAINT "user_credentials_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 "vehicles" ADD CONSTRAINT "vehicles_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_driver_auth_users_id_fk" FOREIGN KEY ("driver") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vehicles" ADD CONSTRAINT "vehicles_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vendors" ADD CONSTRAINT "vendors_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vendors" ADD CONSTRAINT "vendors_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "notifications_preferences_tenant_id_user_id_event_type_chan_key" ON "notifications_preferences" USING btree ("tenant_id","user_id","event_type","channel");--> statement-breakpoint +CREATE UNIQUE INDEX "notifications_preferences_defau_tenant_id_event_key_channel_key" ON "notifications_preferences_defaults" USING btree ("tenant_id","event_key","channel"); \ No newline at end of file diff --git a/db/migrations/0001_medical_big_bertha.sql b/db/migrations/0001_medical_big_bertha.sql new file mode 100644 index 0000000..5ff60a1 --- /dev/null +++ b/db/migrations/0001_medical_big_bertha.sql @@ -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"; \ No newline at end of file diff --git a/db/migrations/0002_silent_christian_walker.sql b/db/migrations/0002_silent_christian_walker.sql new file mode 100644 index 0000000..bd4a372 --- /dev/null +++ b/db/migrations/0002_silent_christian_walker.sql @@ -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; \ No newline at end of file diff --git a/db/migrations/meta/0000_snapshot.json b/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..3fff43c --- /dev/null +++ b/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,9788 @@ +{ + "id": "c74cefc4-5ae7-408c-b7f3-09093efb52b5", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "accounts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_profiles": { + "name": "auth_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "((first_name || ' ') || last_name)", + "type": "stored" + } + }, + "mobile_tel": { + "name": "mobile_tel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_tel": { + "name": "fixed_tel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "employee_number": { + "name": "employee_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_working_hours": { + "name": "weekly_working_hours", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "annual_paid_leave_days": { + "name": "annual_paid_leave_days", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "weekly_regular_working_hours": { + "name": "weekly_regular_working_hours", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "clothing_size_top": { + "name": "clothing_size_top", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clothing_size_bottom": { + "name": "clothing_size_bottom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clothing_size_shoe": { + "name": "clothing_size_shoe", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_signature": { + "name": "email_signature", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'

Mit freundlichen Grüßen

'" + }, + "birthday": { + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "entry_date": { + "name": "entry_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "automatic_hour_corrections": { + "name": "automatic_hour_corrections", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "recreation_days_compensation": { + "name": "recreation_days_compensation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customer_for_portal": { + "name": "customer_for_portal", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "pinned_on_navigation": { + "name": "pinned_on_navigation", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_id": { + "name": "token_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_working_days": { + "name": "weekly_working_days", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "old_profile_id": { + "name": "old_profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "temp_config": { + "name": "temp_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state_code": { + "name": "state_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'DE-NI'" + }, + "contract_type": { + "name": "contract_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "qualification": { + "name": "qualification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "auth_profiles_user_id_auth_users_id_fk": { + "name": "auth_profiles_user_id_auth_users_id_fk", + "tableFrom": "auth_profiles", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_role_permissions": { + "name": "auth_role_permissions", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "auth_role_permissions_role_id_auth_roles_id_fk": { + "name": "auth_role_permissions_role_id_auth_roles_id_fk", + "tableFrom": "auth_role_permissions", + "tableTo": "auth_roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_roles": { + "name": "auth_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_roles_created_by_auth_users_id_fk": { + "name": "auth_roles_created_by_auth_users_id_fk", + "tableFrom": "auth_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_tenant_users": { + "name": "auth_tenant_users", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_tenant_users_created_by_auth_users_id_fk": { + "name": "auth_tenant_users_created_by_auth_users_id_fk", + "tableFrom": "auth_tenant_users", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user_roles": { + "name": "auth_user_roles", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_user_roles_user_id_auth_users_id_fk": { + "name": "auth_user_roles_user_id_auth_users_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "auth_user_roles_role_id_auth_roles_id_fk": { + "name": "auth_user_roles_role_id_auth_roles_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "auth_user_roles_created_by_auth_users_id_fk": { + "name": "auth_user_roles_created_by_auth_users_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_users": { + "name": "auth_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_tenant": { + "name": "multi_tenant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ported": { + "name": "ported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankaccounts": { + "name": "bankaccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "bankaccounts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "iban": { + "name": "iban", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bankId": { + "name": "bankId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ownerName": { + "name": "ownerName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "expired": { + "name": "expired", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "datevNumber": { + "name": "datevNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankaccounts_tenant_tenants_id_fk": { + "name": "bankaccounts_tenant_tenants_id_fk", + "tableFrom": "bankaccounts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankaccounts_updated_by_auth_users_id_fk": { + "name": "bankaccounts_updated_by_auth_users_id_fk", + "tableFrom": "bankaccounts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankrequisitions": { + "name": "bankrequisitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "institutionId": { + "name": "institutionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankrequisitions_tenant_tenants_id_fk": { + "name": "bankrequisitions_tenant_tenants_id_fk", + "tableFrom": "bankrequisitions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankrequisitions_updated_by_auth_users_id_fk": { + "name": "bankrequisitions_updated_by_auth_users_id_fk", + "tableFrom": "bankrequisitions", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankstatements": { + "name": "bankstatements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "bankstatements_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "account": { + "name": "account", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credIban": { + "name": "credIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credName": { + "name": "credName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "debIban": { + "name": "debIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "debName": { + "name": "debName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gocardlessId": { + "name": "gocardlessId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valueDate": { + "name": "valueDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incomingInvoice": { + "name": "incomingInvoice", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "mandateId": { + "name": "mandateId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createdDocument": { + "name": "createdDocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankstatements_account_bankaccounts_id_fk": { + "name": "bankstatements_account_bankaccounts_id_fk", + "tableFrom": "bankstatements", + "tableTo": "bankaccounts", + "columnsFrom": [ + "account" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_tenant_tenants_id_fk": { + "name": "bankstatements_tenant_tenants_id_fk", + "tableFrom": "bankstatements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_incomingInvoice_incominginvoices_id_fk": { + "name": "bankstatements_incomingInvoice_incominginvoices_id_fk", + "tableFrom": "bankstatements", + "tableTo": "incominginvoices", + "columnsFrom": [ + "incomingInvoice" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_contract_contracts_id_fk": { + "name": "bankstatements_contract_contracts_id_fk", + "tableFrom": "bankstatements", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_createdDocument_createddocuments_id_fk": { + "name": "bankstatements_createdDocument_createddocuments_id_fk", + "tableFrom": "bankstatements", + "tableTo": "createddocuments", + "columnsFrom": [ + "createdDocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_updated_by_auth_users_id_fk": { + "name": "bankstatements_updated_by_auth_users_id_fk", + "tableFrom": "bankstatements", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkexecutions": { + "name": "checkexecutions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "checkexecutions_check_checks_id_fk": { + "name": "checkexecutions_check_checks_id_fk", + "tableFrom": "checkexecutions", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checks": { + "name": "checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "distance": { + "name": "distance", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "distanceUnit": { + "name": "distanceUnit", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'days'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "checks_vehicle_vehicles_id_fk": { + "name": "checks_vehicle_vehicles_id_fk", + "tableFrom": "checks", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_inventoryitem_inventoryitems_id_fk": { + "name": "checks_inventoryitem_inventoryitems_id_fk", + "tableFrom": "checks", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_tenant_tenants_id_fk": { + "name": "checks_tenant_tenants_id_fk", + "tableFrom": "checks", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_updated_by_auth_users_id_fk": { + "name": "checks_updated_by_auth_users_id_fk", + "tableFrom": "checks", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.citys": { + "name": "citys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "citys_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long": { + "name": "long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geometry": { + "name": "geometry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "zip": { + "name": "zip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "districtCode": { + "name": "districtCode", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "countryName": { + "name": "countryName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "countryCode": { + "name": "countryCode", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "districtName": { + "name": "districtName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geopoint": { + "name": "geopoint", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "contacts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "phoneMobile": { + "name": "phoneMobile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phoneHome": { + "name": "phoneHome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fullName": { + "name": "fullName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "birthday": { + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "contacts_customer_customers_id_fk": { + "name": "contacts_customer_customers_id_fk", + "tableFrom": "contacts", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contacts_updated_by_auth_users_id_fk": { + "name": "contacts_updated_by_auth_users_id_fk", + "tableFrom": "contacts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contracts": { + "name": "contracts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "contracts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "recurring": { + "name": "recurring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rhythm": { + "name": "rhythm", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "endDate": { + "name": "endDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signDate": { + "name": "signDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bankingIban": { + "name": "bankingIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingBIC": { + "name": "bankingBIC", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingName": { + "name": "bankingName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingOwner": { + "name": "bankingOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sepaRef": { + "name": "sepaRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sepaDate": { + "name": "sepaDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paymentType": { + "name": "paymentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invoiceDispatch": { + "name": "invoiceDispatch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ownFields": { + "name": "ownFields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "contractNumber": { + "name": "contractNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "contracts_customer_customers_id_fk": { + "name": "contracts_customer_customers_id_fk", + "tableFrom": "contracts", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contracts_contact_contacts_id_fk": { + "name": "contracts_contact_contacts_id_fk", + "tableFrom": "contracts", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contracts_updated_by_auth_users_id_fk": { + "name": "contracts_updated_by_auth_users_id_fk", + "tableFrom": "contracts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.costcentres": { + "name": "costcentres", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "costcentres_tenant_tenants_id_fk": { + "name": "costcentres_tenant_tenants_id_fk", + "tableFrom": "costcentres", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_vehicle_vehicles_id_fk": { + "name": "costcentres_vehicle_vehicles_id_fk", + "tableFrom": "costcentres", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_project_projects_id_fk": { + "name": "costcentres_project_projects_id_fk", + "tableFrom": "costcentres", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_inventoryitem_inventoryitems_id_fk": { + "name": "costcentres_inventoryitem_inventoryitems_id_fk", + "tableFrom": "costcentres", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_updated_by_auth_users_id_fk": { + "name": "costcentres_updated_by_auth_users_id_fk", + "tableFrom": "costcentres", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.countrys": { + "name": "countrys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "countrys_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.createddocuments": { + "name": "createddocuments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "createddocuments_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'INVOICE'" + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "documentNumber": { + "name": "documentNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentDate": { + "name": "documentDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Entwurf'" + }, + "info": { + "name": "info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startText": { + "name": "startText", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endText": { + "name": "endText", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rows": { + "name": "rows", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "deliveryDateType": { + "name": "deliveryDateType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paymentDays": { + "name": "paymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "deliveryDate": { + "name": "deliveryDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactPerson": { + "name": "contactPerson", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "serialConfig": { + "name": "serialConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "linkedDocument": { + "name": "linkedDocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "agriculture": { + "name": "agriculture", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "letterhead": { + "name": "letterhead", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "advanceInvoiceResolved": { + "name": "advanceInvoiceResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usedAdvanceInvoices": { + "name": "usedAdvanceInvoices", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deliveryDateEnd": { + "name": "deliveryDateEnd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "taxType": { + "name": "taxType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customSurchargePercentage": { + "name": "customSurchargePercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "availableInPortal": { + "name": "availableInPortal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payment_type": { + "name": "payment_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'transfer'" + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "createddocuments_tenant_tenants_id_fk": { + "name": "createddocuments_tenant_tenants_id_fk", + "tableFrom": "createddocuments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_customer_customers_id_fk": { + "name": "createddocuments_customer_customers_id_fk", + "tableFrom": "createddocuments", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_contact_contacts_id_fk": { + "name": "createddocuments_contact_contacts_id_fk", + "tableFrom": "createddocuments", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_project_projects_id_fk": { + "name": "createddocuments_project_projects_id_fk", + "tableFrom": "createddocuments", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_createdBy_auth_users_id_fk": { + "name": "createddocuments_createdBy_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_linkedDocument_createddocuments_id_fk": { + "name": "createddocuments_linkedDocument_createddocuments_id_fk", + "tableFrom": "createddocuments", + "tableTo": "createddocuments", + "columnsFrom": [ + "linkedDocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_letterhead_letterheads_id_fk": { + "name": "createddocuments_letterhead_letterheads_id_fk", + "tableFrom": "createddocuments", + "tableTo": "letterheads", + "columnsFrom": [ + "letterhead" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_plant_plants_id_fk": { + "name": "createddocuments_plant_plants_id_fk", + "tableFrom": "createddocuments", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_updated_by_auth_users_id_fk": { + "name": "createddocuments_updated_by_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_created_by_auth_users_id_fk": { + "name": "createddocuments_created_by_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_contract_contracts_id_fk": { + "name": "createddocuments_contract_contracts_id_fk", + "tableFrom": "createddocuments", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.createdletters": { + "name": "createdletters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "content_json": { + "name": "content_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "createdletters_tenant_tenants_id_fk": { + "name": "createdletters_tenant_tenants_id_fk", + "tableFrom": "createdletters", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_customer_customers_id_fk": { + "name": "createdletters_customer_customers_id_fk", + "tableFrom": "createdletters", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_vendor_vendors_id_fk": { + "name": "createdletters_vendor_vendors_id_fk", + "tableFrom": "createdletters", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_updated_by_auth_users_id_fk": { + "name": "createdletters_updated_by_auth_users_id_fk", + "tableFrom": "createdletters", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "customers_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "customerNumber": { + "name": "customerNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Privat'" + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isCompany": { + "name": "isCompany", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customPaymentDays": { + "name": "customPaymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastname": { + "name": "lastname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customSurchargePercentage": { + "name": "customSurchargePercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nameAddition": { + "name": "nameAddition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "availableInPortal": { + "name": "availableInPortal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "custom_payment_type": { + "name": "custom_payment_type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "customers_updated_by_auth_users_id_fk": { + "name": "customers_updated_by_auth_users_id_fk", + "tableFrom": "customers", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "devices_tenant_tenants_id_fk": { + "name": "devices_tenant_tenants_id_fk", + "tableFrom": "devices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentboxes": { + "name": "documentboxes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "documentboxes_space_spaces_id_fk": { + "name": "documentboxes_space_spaces_id_fk", + "tableFrom": "documentboxes", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documentboxes_tenant_tenants_id_fk": { + "name": "documentboxes_tenant_tenants_id_fk", + "tableFrom": "documentboxes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documentboxes_updated_by_auth_users_id_fk": { + "name": "documentboxes_updated_by_auth_users_id_fk", + "tableFrom": "documentboxes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startDate": { + "name": "startDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "eventtype": { + "name": "eventtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Umsetzung'" + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "vehicles": { + "name": "vehicles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "inventoryitems": { + "name": "inventoryitems", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "inventoryitemgroups": { + "name": "inventoryitemgroups", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "events_customer_customers_id_fk": { + "name": "events_customer_customers_id_fk", + "tableFrom": "events", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "events_updated_by_auth_users_id_fk": { + "name": "events_updated_by_auth_users_id_fk", + "tableFrom": "events", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "incominginvoice": { + "name": "incominginvoice", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createddocument": { + "name": "createddocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "product": { + "name": "product", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "documentbox": { + "name": "documentbox", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_profile": { + "name": "auth_profile", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "files_tenant_tenants_id_fk": { + "name": "files_tenant_tenants_id_fk", + "tableFrom": "files", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_project_projects_id_fk": { + "name": "files_project_projects_id_fk", + "tableFrom": "files", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_customer_customers_id_fk": { + "name": "files_customer_customers_id_fk", + "tableFrom": "files", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_contract_contracts_id_fk": { + "name": "files_contract_contracts_id_fk", + "tableFrom": "files", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_vendor_vendors_id_fk": { + "name": "files_vendor_vendors_id_fk", + "tableFrom": "files", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_incominginvoice_incominginvoices_id_fk": { + "name": "files_incominginvoice_incominginvoices_id_fk", + "tableFrom": "files", + "tableTo": "incominginvoices", + "columnsFrom": [ + "incominginvoice" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_plant_plants_id_fk": { + "name": "files_plant_plants_id_fk", + "tableFrom": "files", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_createddocument_createddocuments_id_fk": { + "name": "files_createddocument_createddocuments_id_fk", + "tableFrom": "files", + "tableTo": "createddocuments", + "columnsFrom": [ + "createddocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_vehicle_vehicles_id_fk": { + "name": "files_vehicle_vehicles_id_fk", + "tableFrom": "files", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_product_products_id_fk": { + "name": "files_product_products_id_fk", + "tableFrom": "files", + "tableTo": "products", + "columnsFrom": [ + "product" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_check_checks_id_fk": { + "name": "files_check_checks_id_fk", + "tableFrom": "files", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_inventoryitem_inventoryitems_id_fk": { + "name": "files_inventoryitem_inventoryitems_id_fk", + "tableFrom": "files", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_folder_folders_id_fk": { + "name": "files_folder_folders_id_fk", + "tableFrom": "files", + "tableTo": "folders", + "columnsFrom": [ + "folder" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_space_spaces_id_fk": { + "name": "files_space_spaces_id_fk", + "tableFrom": "files", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_type_filetags_id_fk": { + "name": "files_type_filetags_id_fk", + "tableFrom": "files", + "tableTo": "filetags", + "columnsFrom": [ + "type" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_documentbox_documentboxes_id_fk": { + "name": "files_documentbox_documentboxes_id_fk", + "tableFrom": "files", + "tableTo": "documentboxes", + "columnsFrom": [ + "documentbox" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_updated_by_auth_users_id_fk": { + "name": "files_updated_by_auth_users_id_fk", + "tableFrom": "files", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_created_by_auth_users_id_fk": { + "name": "files_created_by_auth_users_id_fk", + "tableFrom": "files", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_auth_profile_auth_profiles_id_fk": { + "name": "files_auth_profile_auth_profiles_id_fk", + "tableFrom": "files", + "tableTo": "auth_profiles", + "columnsFrom": [ + "auth_profile" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.filetags": { + "name": "filetags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createddocumenttype": { + "name": "createddocumenttype", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "incomingDocumentType": { + "name": "incomingDocumentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "filetags_tenant_tenants_id_fk": { + "name": "filetags_tenant_tenants_id_fk", + "tableFrom": "filetags", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folders": { + "name": "folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent": { + "name": "parent", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "isSystemUsed": { + "name": "isSystemUsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "function": { + "name": "function", + "type": "folderfunctions", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "standardFiletype": { + "name": "standardFiletype", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "standardFiletypeIsOptional": { + "name": "standardFiletypeIsOptional", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "folders_tenant_tenants_id_fk": { + "name": "folders_tenant_tenants_id_fk", + "tableFrom": "folders", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_parent_folders_id_fk": { + "name": "folders_parent_folders_id_fk", + "tableFrom": "folders", + "tableTo": "folders", + "columnsFrom": [ + "parent" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_standardFiletype_filetags_id_fk": { + "name": "folders_standardFiletype_filetags_id_fk", + "tableFrom": "folders", + "tableTo": "filetags", + "columnsFrom": [ + "standardFiletype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_updated_by_auth_users_id_fk": { + "name": "folders_updated_by_auth_users_id_fk", + "tableFrom": "folders", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports": { + "name": "exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "exports_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'datev'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "exports_tenant_id_tenants_id_fk": { + "name": "exports_tenant_id_tenants_id_fk", + "tableFrom": "exports", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.globalmessages": { + "name": "globalmessages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "globalmessages_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.globalmessagesseen": { + "name": "globalmessagesseen", + "schema": "", + "columns": { + "message": { + "name": "message", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "globalmessagesseen_message_globalmessages_id_fk": { + "name": "globalmessagesseen_message_globalmessages_id_fk", + "tableFrom": "globalmessagesseen", + "tableTo": "globalmessages", + "columnsFrom": [ + "message" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_channel_instances": { + "name": "helpdesk_channel_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "public_config": { + "name": "public_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "public_token": { + "name": "public_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_token": { + "name": "secret_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_channel_instances_tenant_id_tenants_id_fk": { + "name": "helpdesk_channel_instances_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_channel_instances_type_id_helpdesk_channel_types_id_fk": { + "name": "helpdesk_channel_instances_type_id_helpdesk_channel_types_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "helpdesk_channel_types", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_channel_instances_created_by_auth_users_id_fk": { + "name": "helpdesk_channel_instances_created_by_auth_users_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "helpdesk_channel_instances_public_token_unique": { + "name": "helpdesk_channel_instances_public_token_unique", + "nullsNotDistinct": false, + "columns": [ + "public_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_channel_types": { + "name": "helpdesk_channel_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_contacts": { + "name": "helpdesk_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "source_channel_id": { + "name": "source_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_contacts_tenant_id_tenants_id_fk": { + "name": "helpdesk_contacts_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_contacts_customer_id_customers_id_fk": { + "name": "helpdesk_contacts_customer_id_customers_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_contacts_source_channel_id_helpdesk_channel_instances_id_fk": { + "name": "helpdesk_contacts_source_channel_id_helpdesk_channel_instances_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "helpdesk_channel_instances", + "columnsFrom": [ + "source_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_contacts_contact_id_contacts_id_fk": { + "name": "helpdesk_contacts_contact_id_contacts_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_conversation_participants": { + "name": "helpdesk_conversation_participants", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_conversation_participants_conversation_id_helpdesk_conversations_id_fk": { + "name": "helpdesk_conversation_participants_conversation_id_helpdesk_conversations_id_fk", + "tableFrom": "helpdesk_conversation_participants", + "tableTo": "helpdesk_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversation_participants_user_id_auth_users_id_fk": { + "name": "helpdesk_conversation_participants_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_conversation_participants", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_conversations": { + "name": "helpdesk_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "channel_instance_id": { + "name": "channel_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'normal'" + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "customer_id": { + "name": "customer_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact_person_id": { + "name": "contact_person_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ticket_number": { + "name": "ticket_number", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_conversations_tenant_id_tenants_id_fk": { + "name": "helpdesk_conversations_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversations_channel_instance_id_helpdesk_channel_instances_id_fk": { + "name": "helpdesk_conversations_channel_instance_id_helpdesk_channel_instances_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "helpdesk_channel_instances", + "columnsFrom": [ + "channel_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversations_contact_id_helpdesk_contacts_id_fk": { + "name": "helpdesk_conversations_contact_id_helpdesk_contacts_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "helpdesk_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_conversations_assignee_user_id_auth_users_id_fk": { + "name": "helpdesk_conversations_assignee_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "auth_users", + "columnsFrom": [ + "assignee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_conversations_customer_id_customers_id_fk": { + "name": "helpdesk_conversations_customer_id_customers_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_conversations_contact_person_id_contacts_id_fk": { + "name": "helpdesk_conversations_contact_person_id_contacts_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "contacts", + "columnsFrom": [ + "contact_person_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_messages": { + "name": "helpdesk_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_meta": { + "name": "raw_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_message_id": { + "name": "external_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_messages_tenant_id_tenants_id_fk": { + "name": "helpdesk_messages_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_messages_conversation_id_helpdesk_conversations_id_fk": { + "name": "helpdesk_messages_conversation_id_helpdesk_conversations_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "helpdesk_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_messages_author_user_id_auth_users_id_fk": { + "name": "helpdesk_messages_author_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "auth_users", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_messages_contact_id_helpdesk_contacts_id_fk": { + "name": "helpdesk_messages_contact_id_helpdesk_contacts_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "helpdesk_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "helpdesk_messages_external_message_id_unique": { + "name": "helpdesk_messages_external_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_routing_rules": { + "name": "helpdesk_routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition": { + "name": "condition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_routing_rules_tenant_id_tenants_id_fk": { + "name": "helpdesk_routing_rules_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_routing_rules", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_routing_rules_created_by_auth_users_id_fk": { + "name": "helpdesk_routing_rules_created_by_auth_users_id_fk", + "tableFrom": "helpdesk_routing_rules", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.historyitems": { + "name": "historyitems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "historyitems_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "incomingInvoice": { + "name": "incomingInvoice", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "product": { + "name": "product", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "newVal": { + "name": "newVal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oldVal": { + "name": "oldVal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task": { + "name": "task", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bankstatement": { + "name": "bankstatement", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "projecttype": { + "name": "projecttype", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "service": { + "name": "service", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createddocument": { + "name": "createddocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file": { + "name": "file", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inventoryitemgroup": { + "name": "inventoryitemgroup", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Software'" + }, + "costcentre": { + "name": "costcentre", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownaccount": { + "name": "ownaccount", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "documentbox": { + "name": "documentbox", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hourrate": { + "name": "hourrate", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "historyitems_customer_customers_id_fk": { + "name": "historyitems_customer_customers_id_fk", + "tableFrom": "historyitems", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_tenant_tenants_id_fk": { + "name": "historyitems_tenant_tenants_id_fk", + "tableFrom": "historyitems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_vendor_vendors_id_fk": { + "name": "historyitems_vendor_vendors_id_fk", + "tableFrom": "historyitems", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_project_projects_id_fk": { + "name": "historyitems_project_projects_id_fk", + "tableFrom": "historyitems", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_plant_plants_id_fk": { + "name": "historyitems_plant_plants_id_fk", + "tableFrom": "historyitems", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_incomingInvoice_incominginvoices_id_fk": { + "name": "historyitems_incomingInvoice_incominginvoices_id_fk", + "tableFrom": "historyitems", + "tableTo": "incominginvoices", + "columnsFrom": [ + "incomingInvoice" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_contact_contacts_id_fk": { + "name": "historyitems_contact_contacts_id_fk", + "tableFrom": "historyitems", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_inventoryitem_inventoryitems_id_fk": { + "name": "historyitems_inventoryitem_inventoryitems_id_fk", + "tableFrom": "historyitems", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_product_products_id_fk": { + "name": "historyitems_product_products_id_fk", + "tableFrom": "historyitems", + "tableTo": "products", + "columnsFrom": [ + "product" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_event_events_id_fk": { + "name": "historyitems_event_events_id_fk", + "tableFrom": "historyitems", + "tableTo": "events", + "columnsFrom": [ + "event" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_task_tasks_id_fk": { + "name": "historyitems_task_tasks_id_fk", + "tableFrom": "historyitems", + "tableTo": "tasks", + "columnsFrom": [ + "task" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_vehicle_vehicles_id_fk": { + "name": "historyitems_vehicle_vehicles_id_fk", + "tableFrom": "historyitems", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_bankstatement_bankstatements_id_fk": { + "name": "historyitems_bankstatement_bankstatements_id_fk", + "tableFrom": "historyitems", + "tableTo": "bankstatements", + "columnsFrom": [ + "bankstatement" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_space_spaces_id_fk": { + "name": "historyitems_space_spaces_id_fk", + "tableFrom": "historyitems", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_projecttype_projecttypes_id_fk": { + "name": "historyitems_projecttype_projecttypes_id_fk", + "tableFrom": "historyitems", + "tableTo": "projecttypes", + "columnsFrom": [ + "projecttype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_check_checks_id_fk": { + "name": "historyitems_check_checks_id_fk", + "tableFrom": "historyitems", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_service_services_id_fk": { + "name": "historyitems_service_services_id_fk", + "tableFrom": "historyitems", + "tableTo": "services", + "columnsFrom": [ + "service" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_createddocument_createddocuments_id_fk": { + "name": "historyitems_createddocument_createddocuments_id_fk", + "tableFrom": "historyitems", + "tableTo": "createddocuments", + "columnsFrom": [ + "createddocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_file_files_id_fk": { + "name": "historyitems_file_files_id_fk", + "tableFrom": "historyitems", + "tableTo": "files", + "columnsFrom": [ + "file" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_inventoryitemgroup_inventoryitemgroups_id_fk": { + "name": "historyitems_inventoryitemgroup_inventoryitemgroups_id_fk", + "tableFrom": "historyitems", + "tableTo": "inventoryitemgroups", + "columnsFrom": [ + "inventoryitemgroup" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_costcentre_costcentres_id_fk": { + "name": "historyitems_costcentre_costcentres_id_fk", + "tableFrom": "historyitems", + "tableTo": "costcentres", + "columnsFrom": [ + "costcentre" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_ownaccount_ownaccounts_id_fk": { + "name": "historyitems_ownaccount_ownaccounts_id_fk", + "tableFrom": "historyitems", + "tableTo": "ownaccounts", + "columnsFrom": [ + "ownaccount" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_documentbox_documentboxes_id_fk": { + "name": "historyitems_documentbox_documentboxes_id_fk", + "tableFrom": "historyitems", + "tableTo": "documentboxes", + "columnsFrom": [ + "documentbox" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_hourrate_hourrates_id_fk": { + "name": "historyitems_hourrate_hourrates_id_fk", + "tableFrom": "historyitems", + "tableTo": "hourrates", + "columnsFrom": [ + "hourrate" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_created_by_auth_users_id_fk": { + "name": "historyitems_created_by_auth_users_id_fk", + "tableFrom": "historyitems", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.holidays": { + "name": "holidays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "holidays_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_code": { + "name": "state_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hourrates": { + "name": "hourrates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hourrates_tenant_tenants_id_fk": { + "name": "hourrates_tenant_tenants_id_fk", + "tableFrom": "hourrates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hourrates_updated_by_auth_users_id_fk": { + "name": "hourrates_updated_by_auth_users_id_fk", + "tableFrom": "hourrates", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.incominginvoices": { + "name": "incominginvoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "incominginvoices_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Entwurf'" + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document": { + "name": "document", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paymentType": { + "name": "paymentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accounts": { + "name": "accounts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"account\":null,\"taxType\":null,\"amountNet\":null,\"amountTax\":19,\"costCentre\":null}]'::jsonb" + }, + "paid": { + "name": "paid", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expense": { + "name": "expense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "incominginvoices_tenant_tenants_id_fk": { + "name": "incominginvoices_tenant_tenants_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incominginvoices_vendor_vendors_id_fk": { + "name": "incominginvoices_vendor_vendors_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incominginvoices_updated_by_auth_users_id_fk": { + "name": "incominginvoices_updated_by_auth_users_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventoryitemgroups": { + "name": "inventoryitemgroups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inventoryitems": { + "name": "inventoryitems", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "usePlanning": { + "name": "usePlanning", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "inventoryitemgroups_tenant_tenants_id_fk": { + "name": "inventoryitemgroups_tenant_tenants_id_fk", + "tableFrom": "inventoryitemgroups", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitemgroups_updated_by_auth_users_id_fk": { + "name": "inventoryitemgroups_updated_by_auth_users_id_fk", + "tableFrom": "inventoryitemgroups", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventoryitems": { + "name": "inventoryitems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "inventoryitems_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usePlanning": { + "name": "usePlanning", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currentSpace": { + "name": "currentSpace", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "articleNumber": { + "name": "articleNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialNumber": { + "name": "serialNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchaseDate": { + "name": "purchaseDate", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "manufacturer": { + "name": "manufacturer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manufacturerNumber": { + "name": "manufacturerNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currentValue": { + "name": "currentValue", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "inventoryitems_tenant_tenants_id_fk": { + "name": "inventoryitems_tenant_tenants_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_currentSpace_spaces_id_fk": { + "name": "inventoryitems_currentSpace_spaces_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "spaces", + "columnsFrom": [ + "currentSpace" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_vendor_vendors_id_fk": { + "name": "inventoryitems_vendor_vendors_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_updated_by_auth_users_id_fk": { + "name": "inventoryitems_updated_by_auth_users_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.letterheads": { + "name": "letterheads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "letterheads_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Standard'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentTypes": { + "name": "documentTypes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "letterheads_tenant_tenants_id_fk": { + "name": "letterheads_tenant_tenants_id_fk", + "tableFrom": "letterheads", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "letterheads_updated_by_auth_users_id_fk": { + "name": "letterheads_updated_by_auth_users_id_fk", + "tableFrom": "letterheads", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.movements": { + "name": "movements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "movements_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "productId": { + "name": "productId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spaceId": { + "name": "spaceId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serials": { + "name": "serials", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "movements_productId_products_id_fk": { + "name": "movements_productId_products_id_fk", + "tableFrom": "movements", + "tableTo": "products", + "columnsFrom": [ + "productId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_spaceId_spaces_id_fk": { + "name": "movements_spaceId_spaces_id_fk", + "tableFrom": "movements", + "tableTo": "spaces", + "columnsFrom": [ + "spaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_tenant_tenants_id_fk": { + "name": "movements_tenant_tenants_id_fk", + "tableFrom": "movements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_projectId_projects_id_fk": { + "name": "movements_projectId_projects_id_fk", + "tableFrom": "movements", + "tableTo": "projects", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_updated_by_auth_users_id_fk": { + "name": "movements_updated_by_auth_users_id_fk", + "tableFrom": "movements", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_event_types": { + "name": "notifications_event_types", + "schema": "", + "columns": { + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "notification_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "allowed_channels": { + "name": "allowed_channels", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"inapp\",\"email\"]'::jsonb" + }, + "payload_schema": { + "name": "payload_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_items": { + "name": "notifications_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_items_tenant_id_tenants_id_fk": { + "name": "notifications_items_tenant_id_tenants_id_fk", + "tableFrom": "notifications_items", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_items_user_id_auth_users_id_fk": { + "name": "notifications_items_user_id_auth_users_id_fk", + "tableFrom": "notifications_items", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_items_event_type_notifications_event_types_event_key_fk": { + "name": "notifications_items_event_type_notifications_event_types_event_key_fk", + "tableFrom": "notifications_items", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_type" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_preferences": { + "name": "notifications_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_preferences_tenant_id_user_id_event_type_chan_key": { + "name": "notifications_preferences_tenant_id_user_id_event_type_chan_key", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_preferences_tenant_id_tenants_id_fk": { + "name": "notifications_preferences_tenant_id_tenants_id_fk", + "tableFrom": "notifications_preferences", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_user_id_auth_users_id_fk": { + "name": "notifications_preferences_user_id_auth_users_id_fk", + "tableFrom": "notifications_preferences", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_event_type_notifications_event_types_event_key_fk": { + "name": "notifications_preferences_event_type_notifications_event_types_event_key_fk", + "tableFrom": "notifications_preferences", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_type" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_preferences_defaults": { + "name": "notifications_preferences_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_preferences_defau_tenant_id_event_key_channel_key": { + "name": "notifications_preferences_defau_tenant_id_event_key_channel_key", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_preferences_defaults_tenant_id_tenants_id_fk": { + "name": "notifications_preferences_defaults_tenant_id_tenants_id_fk", + "tableFrom": "notifications_preferences_defaults", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_defaults_event_key_notifications_event_types_event_key_fk": { + "name": "notifications_preferences_defaults_event_key_notifications_event_types_event_key_fk", + "tableFrom": "notifications_preferences_defaults", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_key" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ownaccounts": { + "name": "ownaccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ownaccounts_tenant_tenants_id_fk": { + "name": "ownaccounts_tenant_tenants_id_fk", + "tableFrom": "ownaccounts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ownaccounts_updated_by_auth_users_id_fk": { + "name": "ownaccounts_updated_by_auth_users_id_fk", + "tableFrom": "ownaccounts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plants": { + "name": "plants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "plants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"html\":\"\",\"json\":[],\"text\":\"\"}'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "plants_tenant_tenants_id_fk": { + "name": "plants_tenant_tenants_id_fk", + "tableFrom": "plants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_customer_customers_id_fk": { + "name": "plants_customer_customers_id_fk", + "tableFrom": "plants", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_contract_contracts_id_fk": { + "name": "plants_contract_contracts_id_fk", + "tableFrom": "plants", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_updated_by_auth_users_id_fk": { + "name": "plants_updated_by_auth_users_id_fk", + "tableFrom": "plants", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.productcategories": { + "name": "productcategories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "productcategories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "productcategories_tenant_tenants_id_fk": { + "name": "productcategories_tenant_tenants_id_fk", + "tableFrom": "productcategories", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "productcategories_updated_by_auth_users_id_fk": { + "name": "productcategories_updated_by_auth_users_id_fk", + "tableFrom": "productcategories", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "products_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manufacturer": { + "name": "manufacturer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ean": { + "name": "ean", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "barcode": { + "name": "barcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manufacturerNumber": { + "name": "manufacturerNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendorAllocation": { + "name": "vendorAllocation", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "articleNumber": { + "name": "articleNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "barcodes": { + "name": "barcodes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "productcategories": { + "name": "productcategories", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "taxPercentage": { + "name": "taxPercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 19 + }, + "markupPercentage": { + "name": "markupPercentage", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_unit_units_id_fk": { + "name": "products_unit_units_id_fk", + "tableFrom": "products", + "tableTo": "units", + "columnsFrom": [ + "unit" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_tenant_tenants_id_fk": { + "name": "products_tenant_tenants_id_fk", + "tableFrom": "products", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_updated_by_auth_users_id_fk": { + "name": "products_updated_by_auth_users_id_fk", + "tableFrom": "products", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "projects_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "phases": { + "name": "phases", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "description": { + "name": "description", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "forms": { + "name": "forms", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measure": { + "name": "measure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "projectNumber": { + "name": "projectNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "projectType": { + "name": "projectType", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Projekt'" + }, + "projecttype": { + "name": "projecttype", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customerRef": { + "name": "customerRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_phase": { + "name": "active_phase", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_tenant_tenants_id_fk": { + "name": "projects_tenant_tenants_id_fk", + "tableFrom": "projects", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_customer_customers_id_fk": { + "name": "projects_customer_customers_id_fk", + "tableFrom": "projects", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_contract_contracts_id_fk": { + "name": "projects_contract_contracts_id_fk", + "tableFrom": "projects", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_projecttype_projecttypes_id_fk": { + "name": "projects_projecttype_projecttypes_id_fk", + "tableFrom": "projects", + "tableTo": "projecttypes", + "columnsFrom": [ + "projecttype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_updated_by_auth_users_id_fk": { + "name": "projects_updated_by_auth_users_id_fk", + "tableFrom": "projects", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projecttypes": { + "name": "projecttypes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "projecttypes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initialPhases": { + "name": "initialPhases", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "addablePhases": { + "name": "addablePhases", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projecttypes_tenant_tenants_id_fk": { + "name": "projecttypes_tenant_tenants_id_fk", + "tableFrom": "projecttypes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projecttypes_updated_by_auth_users_id_fk": { + "name": "projecttypes_updated_by_auth_users_id_fk", + "tableFrom": "projecttypes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.servicecategories": { + "name": "servicecategories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "servicecategories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "servicecategories_tenant_tenants_id_fk": { + "name": "servicecategories_tenant_tenants_id_fk", + "tableFrom": "servicecategories", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "servicecategories_updated_by_auth_users_id_fk": { + "name": "servicecategories_updated_by_auth_users_id_fk", + "tableFrom": "servicecategories", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.services": { + "name": "services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "services_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "serviceNumber": { + "name": "serviceNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "servicecategories": { + "name": "servicecategories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "purchasePriceComposed": { + "name": "purchasePriceComposed", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"total\":0}'::jsonb" + }, + "sellingPriceComposed": { + "name": "sellingPriceComposed", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"total\":0}'::jsonb" + }, + "taxPercentage": { + "name": "taxPercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 19 + }, + "materialComposition": { + "name": "materialComposition", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "personalComposition": { + "name": "personalComposition", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "services_tenant_tenants_id_fk": { + "name": "services_tenant_tenants_id_fk", + "tableFrom": "services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_unit_units_id_fk": { + "name": "services_unit_units_id_fk", + "tableFrom": "services", + "tableTo": "units", + "columnsFrom": [ + "unit" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_updated_by_auth_users_id_fk": { + "name": "services_updated_by_auth_users_id_fk", + "tableFrom": "services", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spaces": { + "name": "spaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "spaces_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spaceNumber": { + "name": "spaceNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parentSpace": { + "name": "parentSpace", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"zip\":\"\",\"city\":\"\",\"streetNumber\":\"\"}'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spaces_tenant_tenants_id_fk": { + "name": "spaces_tenant_tenants_id_fk", + "tableFrom": "spaces", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spaces_parentSpace_spaces_id_fk": { + "name": "spaces_parentSpace_spaces_id_fk", + "tableFrom": "spaces", + "tableTo": "spaces", + "columnsFrom": [ + "parentSpace" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spaces_updated_by_auth_users_id_fk": { + "name": "spaces_updated_by_auth_users_id_fk", + "tableFrom": "spaces", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_time_entries": { + "name": "staff_time_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE \n WHEN stopped_at IS NOT NULL \n THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)\n ELSE NULL\n END", + "type": "stored" + } + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'work'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "times_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "device": { + "name": "device", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "internal_note": { + "name": "internal_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vacation_reason": { + "name": "vacation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vacation_days": { + "name": "vacation_days", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sick_reason": { + "name": "sick_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "staff_time_entries_tenant_id_tenants_id_fk": { + "name": "staff_time_entries_tenant_id_tenants_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_time_entries_user_id_auth_users_id_fk": { + "name": "staff_time_entries_user_id_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "staff_time_entries_updated_by_auth_users_id_fk": { + "name": "staff_time_entries_updated_by_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_time_entries_approved_by_auth_users_id_fk": { + "name": "staff_time_entries_approved_by_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_time_entry_connects": { + "name": "staff_time_entry_connects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "time_entry_id": { + "name": "time_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)", + "type": "stored" + } + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "staff_time_entry_connects_time_entry_id_staff_time_entries_id_fk": { + "name": "staff_time_entry_connects_time_entry_id_staff_time_entries_id_fk", + "tableFrom": "staff_time_entry_connects", + "tableTo": "staff_time_entries", + "columnsFrom": [ + "time_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_zeitstromtimestamps": { + "name": "staff_zeitstromtimestamps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "staff_time_entry": { + "name": "staff_time_entry", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "internal_note": { + "name": "internal_note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "staff_zeitstromtimestamps_tenant_tenants_id_fk": { + "name": "staff_zeitstromtimestamps_tenant_tenants_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_zeitstromtimestamps_profile_auth_profiles_id_fk": { + "name": "staff_zeitstromtimestamps_profile_auth_profiles_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "auth_profiles", + "columnsFrom": [ + "profile" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_zeitstromtimestamps_staff_time_entry_staff_time_entries_id_fk": { + "name": "staff_zeitstromtimestamps_staff_time_entry_staff_time_entries_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "staff_time_entries", + "columnsFrom": [ + "staff_time_entry" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.statementallocations": { + "name": "statementallocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bs_id": { + "name": "bs_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cd_id": { + "name": "cd_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ii_id": { + "name": "ii_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "account": { + "name": "account", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "ownaccount": { + "name": "ownaccount", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "statementallocations_bs_id_bankstatements_id_fk": { + "name": "statementallocations_bs_id_bankstatements_id_fk", + "tableFrom": "statementallocations", + "tableTo": "bankstatements", + "columnsFrom": [ + "bs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_cd_id_createddocuments_id_fk": { + "name": "statementallocations_cd_id_createddocuments_id_fk", + "tableFrom": "statementallocations", + "tableTo": "createddocuments", + "columnsFrom": [ + "cd_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_ii_id_incominginvoices_id_fk": { + "name": "statementallocations_ii_id_incominginvoices_id_fk", + "tableFrom": "statementallocations", + "tableTo": "incominginvoices", + "columnsFrom": [ + "ii_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_tenant_tenants_id_fk": { + "name": "statementallocations_tenant_tenants_id_fk", + "tableFrom": "statementallocations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_account_accounts_id_fk": { + "name": "statementallocations_account_accounts_id_fk", + "tableFrom": "statementallocations", + "tableTo": "accounts", + "columnsFrom": [ + "account" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_ownaccount_ownaccounts_id_fk": { + "name": "statementallocations_ownaccount_ownaccounts_id_fk", + "tableFrom": "statementallocations", + "tableTo": "ownaccounts", + "columnsFrom": [ + "ownaccount" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_customer_customers_id_fk": { + "name": "statementallocations_customer_customers_id_fk", + "tableFrom": "statementallocations", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_vendor_vendors_id_fk": { + "name": "statementallocations_vendor_vendors_id_fk", + "tableFrom": "statementallocations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_updated_by_auth_users_id_fk": { + "name": "statementallocations_updated_by_auth_users_id_fk", + "tableFrom": "statementallocations", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tasks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categorie": { + "name": "categorie", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_tenant_tenants_id_fk": { + "name": "tasks_tenant_tenants_id_fk", + "tableFrom": "tasks", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_user_id_auth_users_id_fk": { + "name": "tasks_user_id_auth_users_id_fk", + "tableFrom": "tasks", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_customer_customers_id_fk": { + "name": "tasks_customer_customers_id_fk", + "tableFrom": "tasks", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_updated_by_auth_users_id_fk": { + "name": "tasks_updated_by_auth_users_id_fk", + "tableFrom": "tasks", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.taxtypes": { + "name": "taxtypes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "taxtypes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percentage": { + "name": "percentage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "taxtypes_updated_by_auth_users_id_fk": { + "name": "taxtypes_updated_by_auth_users_id_fk", + "tableFrom": "taxtypes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tenants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendarConfig": { + "name": "calendarConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"eventTypes\":[{\"color\":\"blue\",\"label\":\"Büro\"},{\"color\":\"yellow\",\"label\":\"Besprechung\"},{\"color\":\"green\",\"label\":\"Umsetzung\"},{\"color\":\"red\",\"label\":\"Vor Ort Termin\"}]}'::jsonb" + }, + "timeConfig": { + "name": "timeConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"products\":[],\"documents\":[]}'::jsonb" + }, + "measures": { + "name": "measures", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "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\"}]'::jsonb" + }, + "businessInfo": { + "name": "businessInfo", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"zip\":\"\",\"city\":\"\",\"name\":\"\",\"street\":\"\"}'::jsonb" + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"objects\":true,\"calendar\":true,\"contacts\":true,\"projects\":true,\"vehicles\":true,\"contracts\":true,\"inventory\":true,\"accounting\":true,\"timeTracking\":true,\"planningBoard\":true,\"workingTimeTracking\":true}'::jsonb" + }, + "ownFields": { + "name": "ownFields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "numberRanges": { + "name": "numberRanges", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "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}}'::jsonb" + }, + "standardEmailForInvoices": { + "name": "standardEmailForInvoices", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraModules": { + "name": "extraModules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "isInTrial": { + "name": "isInTrial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trialEndDate": { + "name": "trialEndDate", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasActiveLicense": { + "name": "hasActiveLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "userLicenseCount": { + "name": "userLicenseCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "workstationLicenseCount": { + "name": "workstationLicenseCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "standardPaymentDays": { + "name": "standardPaymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 14 + }, + "dokuboxEmailAddresses": { + "name": "dokuboxEmailAddresses", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "dokuboxkey": { + "name": "dokuboxkey", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "autoPrepareIncomingInvoices": { + "name": "autoPrepareIncomingInvoices", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "portalDomain": { + "name": "portalDomain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portalConfig": { + "name": "portalConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"primayColor\":\"#69c350\"}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "locked_tenant", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tenants_updated_by_auth_users_id_fk": { + "name": "tenants_updated_by_auth_users_id_fk", + "tableFrom": "tenants", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.texttemplates": { + "name": "texttemplates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "texttemplates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentType": { + "name": "documentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pos": { + "name": "pos", + "type": "texttemplatepositions", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "texttemplates_tenant_tenants_id_fk": { + "name": "texttemplates_tenant_tenants_id_fk", + "tableFrom": "texttemplates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "texttemplates_updated_by_auth_users_id_fk": { + "name": "texttemplates_updated_by_auth_users_id_fk", + "tableFrom": "texttemplates", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.units": { + "name": "units", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "units_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "single": { + "name": "single", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multiple": { + "name": "multiple", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_credentials": { + "name": "user_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "smtp_port": { + "name": "smtp_port", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "smtp_ssl": { + "name": "smtp_ssl", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "imap_port": { + "name": "imap_port", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "imap_ssl": { + "name": "imap_ssl", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email_encrypted": { + "name": "email_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "smtp_host_encrypted": { + "name": "smtp_host_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "imap_host_encrypted": { + "name": "imap_host_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_credentials_user_id_auth_users_id_fk": { + "name": "user_credentials_user_id_auth_users_id_fk", + "tableFrom": "user_credentials", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_credentials_tenant_id_tenants_id_fk": { + "name": "user_credentials_tenant_id_tenants_id_fk", + "tableFrom": "user_credentials", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles": { + "name": "vehicles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "vehicles_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "licensePlate": { + "name": "licensePlate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "driver": { + "name": "driver", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "vin": { + "name": "vin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tankSize": { + "name": "tankSize", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildYear": { + "name": "buildYear", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "towingCapacity": { + "name": "towingCapacity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "powerInKW": { + "name": "powerInKW", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "vehicles_tenant_tenants_id_fk": { + "name": "vehicles_tenant_tenants_id_fk", + "tableFrom": "vehicles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vehicles_driver_auth_users_id_fk": { + "name": "vehicles_driver_auth_users_id_fk", + "tableFrom": "vehicles", + "tableTo": "auth_users", + "columnsFrom": [ + "driver" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vehicles_updated_by_auth_users_id_fk": { + "name": "vehicles_updated_by_auth_users_id_fk", + "tableFrom": "vehicles", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors": { + "name": "vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "vendors_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendorNumber": { + "name": "vendorNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasSEPA": { + "name": "hasSEPA", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "defaultPaymentMethod": { + "name": "defaultPaymentMethod", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendors_tenant_tenants_id_fk": { + "name": "vendors_tenant_tenants_id_fk", + "tableFrom": "vendors", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendors_updated_by_auth_users_id_fk": { + "name": "vendors_updated_by_auth_users_id_fk", + "tableFrom": "vendors", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.credential_types": { + "name": "credential_types", + "schema": "public", + "values": [ + "mail", + "m365" + ] + }, + "public.folderfunctions": { + "name": "folderfunctions", + "schema": "public", + "values": [ + "none", + "yearSubCategory", + "incomingInvoices", + "invoices", + "quotes", + "confirmationOrders", + "deliveryNotes", + "vehicleData", + "reminders", + "taxData", + "deposit", + "timeEvaluations" + ] + }, + "public.locked_tenant": { + "name": "locked_tenant", + "schema": "public", + "values": [ + "maintenance_tenant", + "maintenance", + "general", + "no_subscription" + ] + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "public", + "values": [ + "email", + "inapp", + "sms", + "push", + "webhook" + ] + }, + "public.notification_severity": { + "name": "notification_severity", + "schema": "public", + "values": [ + "info", + "success", + "warning", + "error" + ] + }, + "public.notification_status": { + "name": "notification_status", + "schema": "public", + "values": [ + "queued", + "sent", + "failed", + "read" + ] + }, + "public.payment_types": { + "name": "payment_types", + "schema": "public", + "values": [ + "transfer", + "direct_debit" + ] + }, + "public.texttemplatepositions": { + "name": "texttemplatepositions", + "schema": "public", + "values": [ + "startText", + "endText" + ] + }, + "public.times_state": { + "name": "times_state", + "schema": "public", + "values": [ + "submitted", + "approved", + "draft" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0001_snapshot.json b/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..f5a2f32 --- /dev/null +++ b/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,9947 @@ +{ + "id": "91737e8e-9323-4b3f-9c03-13793e3b160c", + "prevId": "c74cefc4-5ae7-408c-b7f3-09093efb52b5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "accounts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_profiles": { + "name": "auth_profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "((first_name || ' ') || last_name)", + "type": "stored" + } + }, + "mobile_tel": { + "name": "mobile_tel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fixed_tel": { + "name": "fixed_tel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "employee_number": { + "name": "employee_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_working_hours": { + "name": "weekly_working_hours", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "annual_paid_leave_days": { + "name": "annual_paid_leave_days", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "weekly_regular_working_hours": { + "name": "weekly_regular_working_hours", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "clothing_size_top": { + "name": "clothing_size_top", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clothing_size_bottom": { + "name": "clothing_size_bottom", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "clothing_size_shoe": { + "name": "clothing_size_shoe", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_signature": { + "name": "email_signature", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'

Mit freundlichen Grüßen

'" + }, + "birthday": { + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "entry_date": { + "name": "entry_date", + "type": "date", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "automatic_hour_corrections": { + "name": "automatic_hour_corrections", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "recreation_days_compensation": { + "name": "recreation_days_compensation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customer_for_portal": { + "name": "customer_for_portal", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "pinned_on_navigation": { + "name": "pinned_on_navigation", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_id": { + "name": "token_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weekly_working_days": { + "name": "weekly_working_days", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "old_profile_id": { + "name": "old_profile_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "temp_config": { + "name": "temp_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state_code": { + "name": "state_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'DE-NI'" + }, + "contract_type": { + "name": "contract_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "qualification": { + "name": "qualification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_street": { + "name": "address_street", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_zip": { + "name": "address_zip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address_city": { + "name": "address_city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "auth_profiles_user_id_auth_users_id_fk": { + "name": "auth_profiles_user_id_auth_users_id_fk", + "tableFrom": "auth_profiles", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_role_permissions": { + "name": "auth_role_permissions", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "auth_role_permissions_role_id_auth_roles_id_fk": { + "name": "auth_role_permissions_role_id_auth_roles_id_fk", + "tableFrom": "auth_role_permissions", + "tableTo": "auth_roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_roles": { + "name": "auth_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_roles_created_by_auth_users_id_fk": { + "name": "auth_roles_created_by_auth_users_id_fk", + "tableFrom": "auth_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_tenant_users": { + "name": "auth_tenant_users", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_tenant_users_created_by_auth_users_id_fk": { + "name": "auth_tenant_users_created_by_auth_users_id_fk", + "tableFrom": "auth_tenant_users", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user_roles": { + "name": "auth_user_roles", + "schema": "", + "columns": { + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "auth_user_roles_user_id_auth_users_id_fk": { + "name": "auth_user_roles_user_id_auth_users_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "auth_user_roles_role_id_auth_roles_id_fk": { + "name": "auth_user_roles_role_id_auth_roles_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "auth_user_roles_created_by_auth_users_id_fk": { + "name": "auth_user_roles_created_by_auth_users_id_fk", + "tableFrom": "auth_user_roles", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_users": { + "name": "auth_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multi_tenant": { + "name": "multi_tenant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "must_change_password": { + "name": "must_change_password", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ported": { + "name": "ported", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankaccounts": { + "name": "bankaccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "bankaccounts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "iban": { + "name": "iban", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "bankId": { + "name": "bankId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ownerName": { + "name": "ownerName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "expired": { + "name": "expired", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "datevNumber": { + "name": "datevNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankaccounts_tenant_tenants_id_fk": { + "name": "bankaccounts_tenant_tenants_id_fk", + "tableFrom": "bankaccounts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankaccounts_updated_by_auth_users_id_fk": { + "name": "bankaccounts_updated_by_auth_users_id_fk", + "tableFrom": "bankaccounts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankrequisitions": { + "name": "bankrequisitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "institutionId": { + "name": "institutionId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankrequisitions_tenant_tenants_id_fk": { + "name": "bankrequisitions_tenant_tenants_id_fk", + "tableFrom": "bankrequisitions", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankrequisitions_updated_by_auth_users_id_fk": { + "name": "bankrequisitions_updated_by_auth_users_id_fk", + "tableFrom": "bankrequisitions", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bankstatements": { + "name": "bankstatements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "bankstatements_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "account": { + "name": "account", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credIban": { + "name": "credIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credName": { + "name": "credName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "debIban": { + "name": "debIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "debName": { + "name": "debName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gocardlessId": { + "name": "gocardlessId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "valueDate": { + "name": "valueDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mandateId": { + "name": "mandateId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "bankstatements_account_bankaccounts_id_fk": { + "name": "bankstatements_account_bankaccounts_id_fk", + "tableFrom": "bankstatements", + "tableTo": "bankaccounts", + "columnsFrom": [ + "account" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_tenant_tenants_id_fk": { + "name": "bankstatements_tenant_tenants_id_fk", + "tableFrom": "bankstatements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_contract_contracts_id_fk": { + "name": "bankstatements_contract_contracts_id_fk", + "tableFrom": "bankstatements", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bankstatements_updated_by_auth_users_id_fk": { + "name": "bankstatements_updated_by_auth_users_id_fk", + "tableFrom": "bankstatements", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checkexecutions": { + "name": "checkexecutions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "checkexecutions_check_checks_id_fk": { + "name": "checkexecutions_check_checks_id_fk", + "tableFrom": "checkexecutions", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.checks": { + "name": "checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "distance": { + "name": "distance", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "distanceUnit": { + "name": "distanceUnit", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'days'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "checks_vehicle_vehicles_id_fk": { + "name": "checks_vehicle_vehicles_id_fk", + "tableFrom": "checks", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_inventoryitem_inventoryitems_id_fk": { + "name": "checks_inventoryitem_inventoryitems_id_fk", + "tableFrom": "checks", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_tenant_tenants_id_fk": { + "name": "checks_tenant_tenants_id_fk", + "tableFrom": "checks", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "checks_updated_by_auth_users_id_fk": { + "name": "checks_updated_by_auth_users_id_fk", + "tableFrom": "checks", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.citys": { + "name": "citys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "citys_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "long": { + "name": "long", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geometry": { + "name": "geometry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "zip": { + "name": "zip", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "districtCode": { + "name": "districtCode", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "countryName": { + "name": "countryName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "countryCode": { + "name": "countryCode", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "districtName": { + "name": "districtName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "geopoint": { + "name": "geopoint", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "contacts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "firstName": { + "name": "firstName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastName": { + "name": "lastName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "phoneMobile": { + "name": "phoneMobile", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phoneHome": { + "name": "phoneHome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fullName": { + "name": "fullName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "birthday": { + "name": "birthday", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "contacts_customer_customers_id_fk": { + "name": "contacts_customer_customers_id_fk", + "tableFrom": "contacts", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contacts_updated_by_auth_users_id_fk": { + "name": "contacts_updated_by_auth_users_id_fk", + "tableFrom": "contacts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contracts": { + "name": "contracts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "contracts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "recurring": { + "name": "recurring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rhythm": { + "name": "rhythm", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "startDate": { + "name": "startDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "endDate": { + "name": "endDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signDate": { + "name": "signDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bankingIban": { + "name": "bankingIban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingBIC": { + "name": "bankingBIC", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingName": { + "name": "bankingName", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankingOwner": { + "name": "bankingOwner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sepaRef": { + "name": "sepaRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sepaDate": { + "name": "sepaDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paymentType": { + "name": "paymentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invoiceDispatch": { + "name": "invoiceDispatch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ownFields": { + "name": "ownFields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "contractNumber": { + "name": "contractNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "contracts_customer_customers_id_fk": { + "name": "contracts_customer_customers_id_fk", + "tableFrom": "contracts", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contracts_contact_contacts_id_fk": { + "name": "contracts_contact_contacts_id_fk", + "tableFrom": "contracts", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "contracts_updated_by_auth_users_id_fk": { + "name": "contracts_updated_by_auth_users_id_fk", + "tableFrom": "contracts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.costcentres": { + "name": "costcentres", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "costcentres_tenant_tenants_id_fk": { + "name": "costcentres_tenant_tenants_id_fk", + "tableFrom": "costcentres", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_vehicle_vehicles_id_fk": { + "name": "costcentres_vehicle_vehicles_id_fk", + "tableFrom": "costcentres", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_project_projects_id_fk": { + "name": "costcentres_project_projects_id_fk", + "tableFrom": "costcentres", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_inventoryitem_inventoryitems_id_fk": { + "name": "costcentres_inventoryitem_inventoryitems_id_fk", + "tableFrom": "costcentres", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "costcentres_updated_by_auth_users_id_fk": { + "name": "costcentres_updated_by_auth_users_id_fk", + "tableFrom": "costcentres", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.countrys": { + "name": "countrys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "countrys_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.createddocuments": { + "name": "createddocuments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "createddocuments_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'INVOICE'" + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "documentNumber": { + "name": "documentNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentDate": { + "name": "documentDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Entwurf'" + }, + "info": { + "name": "info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdBy": { + "name": "createdBy", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "startText": { + "name": "startText", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endText": { + "name": "endText", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rows": { + "name": "rows", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "deliveryDateType": { + "name": "deliveryDateType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paymentDays": { + "name": "paymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "deliveryDate": { + "name": "deliveryDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contactPerson": { + "name": "contactPerson", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "serialConfig": { + "name": "serialConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "linkedDocument": { + "name": "linkedDocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "agriculture": { + "name": "agriculture", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "letterhead": { + "name": "letterhead", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "advanceInvoiceResolved": { + "name": "advanceInvoiceResolved", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "usedAdvanceInvoices": { + "name": "usedAdvanceInvoices", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deliveryDateEnd": { + "name": "deliveryDateEnd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "taxType": { + "name": "taxType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customSurchargePercentage": { + "name": "customSurchargePercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "availableInPortal": { + "name": "availableInPortal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payment_type": { + "name": "payment_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'transfer'" + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "createddocuments_tenant_tenants_id_fk": { + "name": "createddocuments_tenant_tenants_id_fk", + "tableFrom": "createddocuments", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_customer_customers_id_fk": { + "name": "createddocuments_customer_customers_id_fk", + "tableFrom": "createddocuments", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_contact_contacts_id_fk": { + "name": "createddocuments_contact_contacts_id_fk", + "tableFrom": "createddocuments", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_project_projects_id_fk": { + "name": "createddocuments_project_projects_id_fk", + "tableFrom": "createddocuments", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_createdBy_auth_users_id_fk": { + "name": "createddocuments_createdBy_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_linkedDocument_createddocuments_id_fk": { + "name": "createddocuments_linkedDocument_createddocuments_id_fk", + "tableFrom": "createddocuments", + "tableTo": "createddocuments", + "columnsFrom": [ + "linkedDocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_letterhead_letterheads_id_fk": { + "name": "createddocuments_letterhead_letterheads_id_fk", + "tableFrom": "createddocuments", + "tableTo": "letterheads", + "columnsFrom": [ + "letterhead" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_plant_plants_id_fk": { + "name": "createddocuments_plant_plants_id_fk", + "tableFrom": "createddocuments", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_updated_by_auth_users_id_fk": { + "name": "createddocuments_updated_by_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_created_by_auth_users_id_fk": { + "name": "createddocuments_created_by_auth_users_id_fk", + "tableFrom": "createddocuments", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createddocuments_contract_contracts_id_fk": { + "name": "createddocuments_contract_contracts_id_fk", + "tableFrom": "createddocuments", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.createdletters": { + "name": "createdletters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "content_json": { + "name": "content_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "createdletters_tenant_tenants_id_fk": { + "name": "createdletters_tenant_tenants_id_fk", + "tableFrom": "createdletters", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_customer_customers_id_fk": { + "name": "createdletters_customer_customers_id_fk", + "tableFrom": "createdletters", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_vendor_vendors_id_fk": { + "name": "createdletters_vendor_vendors_id_fk", + "tableFrom": "createdletters", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "createdletters_updated_by_auth_users_id_fk": { + "name": "createdletters_updated_by_auth_users_id_fk", + "tableFrom": "createdletters", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "customers_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "customerNumber": { + "name": "customerNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Privat'" + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isCompany": { + "name": "isCompany", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customPaymentDays": { + "name": "customPaymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "firstname": { + "name": "firstname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastname": { + "name": "lastname", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customSurchargePercentage": { + "name": "customSurchargePercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "salutation": { + "name": "salutation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nameAddition": { + "name": "nameAddition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "availableInPortal": { + "name": "availableInPortal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "custom_payment_type": { + "name": "custom_payment_type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "customers_updated_by_auth_users_id_fk": { + "name": "customers_updated_by_auth_users_id_fk", + "tableFrom": "customers", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "externalId": { + "name": "externalId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "devices_tenant_tenants_id_fk": { + "name": "devices_tenant_tenants_id_fk", + "tableFrom": "devices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentboxes": { + "name": "documentboxes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "documentboxes_space_spaces_id_fk": { + "name": "documentboxes_space_spaces_id_fk", + "tableFrom": "documentboxes", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documentboxes_tenant_tenants_id_fk": { + "name": "documentboxes_tenant_tenants_id_fk", + "tableFrom": "documentboxes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documentboxes_updated_by_auth_users_id_fk": { + "name": "documentboxes_updated_by_auth_users_id_fk", + "tableFrom": "documentboxes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startDate": { + "name": "startDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "eventtype": { + "name": "eventtype", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Umsetzung'" + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "vehicles": { + "name": "vehicles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "inventoryitems": { + "name": "inventoryitems", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "inventoryitemgroups": { + "name": "inventoryitemgroups", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "events_customer_customers_id_fk": { + "name": "events_customer_customers_id_fk", + "tableFrom": "events", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "events_updated_by_auth_users_id_fk": { + "name": "events_updated_by_auth_users_id_fk", + "tableFrom": "events", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "incominginvoice": { + "name": "incominginvoice", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createddocument": { + "name": "createddocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "product": { + "name": "product", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "folder": { + "name": "folder", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mimeType": { + "name": "mimeType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "documentbox": { + "name": "documentbox", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auth_profile": { + "name": "auth_profile", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "files_tenant_tenants_id_fk": { + "name": "files_tenant_tenants_id_fk", + "tableFrom": "files", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_project_projects_id_fk": { + "name": "files_project_projects_id_fk", + "tableFrom": "files", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_customer_customers_id_fk": { + "name": "files_customer_customers_id_fk", + "tableFrom": "files", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_contract_contracts_id_fk": { + "name": "files_contract_contracts_id_fk", + "tableFrom": "files", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_vendor_vendors_id_fk": { + "name": "files_vendor_vendors_id_fk", + "tableFrom": "files", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_incominginvoice_incominginvoices_id_fk": { + "name": "files_incominginvoice_incominginvoices_id_fk", + "tableFrom": "files", + "tableTo": "incominginvoices", + "columnsFrom": [ + "incominginvoice" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_plant_plants_id_fk": { + "name": "files_plant_plants_id_fk", + "tableFrom": "files", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_createddocument_createddocuments_id_fk": { + "name": "files_createddocument_createddocuments_id_fk", + "tableFrom": "files", + "tableTo": "createddocuments", + "columnsFrom": [ + "createddocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_vehicle_vehicles_id_fk": { + "name": "files_vehicle_vehicles_id_fk", + "tableFrom": "files", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_product_products_id_fk": { + "name": "files_product_products_id_fk", + "tableFrom": "files", + "tableTo": "products", + "columnsFrom": [ + "product" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_check_checks_id_fk": { + "name": "files_check_checks_id_fk", + "tableFrom": "files", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_inventoryitem_inventoryitems_id_fk": { + "name": "files_inventoryitem_inventoryitems_id_fk", + "tableFrom": "files", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_folder_folders_id_fk": { + "name": "files_folder_folders_id_fk", + "tableFrom": "files", + "tableTo": "folders", + "columnsFrom": [ + "folder" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_space_spaces_id_fk": { + "name": "files_space_spaces_id_fk", + "tableFrom": "files", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_type_filetags_id_fk": { + "name": "files_type_filetags_id_fk", + "tableFrom": "files", + "tableTo": "filetags", + "columnsFrom": [ + "type" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_documentbox_documentboxes_id_fk": { + "name": "files_documentbox_documentboxes_id_fk", + "tableFrom": "files", + "tableTo": "documentboxes", + "columnsFrom": [ + "documentbox" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_updated_by_auth_users_id_fk": { + "name": "files_updated_by_auth_users_id_fk", + "tableFrom": "files", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_created_by_auth_users_id_fk": { + "name": "files_created_by_auth_users_id_fk", + "tableFrom": "files", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "files_auth_profile_auth_profiles_id_fk": { + "name": "files_auth_profile_auth_profiles_id_fk", + "tableFrom": "files", + "tableTo": "auth_profiles", + "columnsFrom": [ + "auth_profile" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.filetags": { + "name": "filetags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "createddocumenttype": { + "name": "createddocumenttype", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "incomingDocumentType": { + "name": "incomingDocumentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "filetags_tenant_tenants_id_fk": { + "name": "filetags_tenant_tenants_id_fk", + "tableFrom": "filetags", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.folders": { + "name": "folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent": { + "name": "parent", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "isSystemUsed": { + "name": "isSystemUsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "function": { + "name": "function", + "type": "folderfunctions", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "standardFiletype": { + "name": "standardFiletype", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "standardFiletypeIsOptional": { + "name": "standardFiletypeIsOptional", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "folders_tenant_tenants_id_fk": { + "name": "folders_tenant_tenants_id_fk", + "tableFrom": "folders", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_parent_folders_id_fk": { + "name": "folders_parent_folders_id_fk", + "tableFrom": "folders", + "tableTo": "folders", + "columnsFrom": [ + "parent" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_standardFiletype_filetags_id_fk": { + "name": "folders_standardFiletype_filetags_id_fk", + "tableFrom": "folders", + "tableTo": "filetags", + "columnsFrom": [ + "standardFiletype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folders_updated_by_auth_users_id_fk": { + "name": "folders_updated_by_auth_users_id_fk", + "tableFrom": "folders", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports": { + "name": "exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "exports_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'datev'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "exports_tenant_id_tenants_id_fk": { + "name": "exports_tenant_id_tenants_id_fk", + "tableFrom": "exports", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.globalmessages": { + "name": "globalmessages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "globalmessages_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.globalmessagesseen": { + "name": "globalmessagesseen", + "schema": "", + "columns": { + "message": { + "name": "message", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "seen_at": { + "name": "seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "globalmessagesseen_message_globalmessages_id_fk": { + "name": "globalmessagesseen_message_globalmessages_id_fk", + "tableFrom": "globalmessagesseen", + "tableTo": "globalmessages", + "columnsFrom": [ + "message" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_channel_instances": { + "name": "helpdesk_channel_instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "public_config": { + "name": "public_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "public_token": { + "name": "public_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_token": { + "name": "secret_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_channel_instances_tenant_id_tenants_id_fk": { + "name": "helpdesk_channel_instances_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_channel_instances_type_id_helpdesk_channel_types_id_fk": { + "name": "helpdesk_channel_instances_type_id_helpdesk_channel_types_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "helpdesk_channel_types", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_channel_instances_created_by_auth_users_id_fk": { + "name": "helpdesk_channel_instances_created_by_auth_users_id_fk", + "tableFrom": "helpdesk_channel_instances", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "helpdesk_channel_instances_public_token_unique": { + "name": "helpdesk_channel_instances_public_token_unique", + "nullsNotDistinct": false, + "columns": [ + "public_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_channel_types": { + "name": "helpdesk_channel_types", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_contacts": { + "name": "helpdesk_contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "source_channel_id": { + "name": "source_channel_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_contacts_tenant_id_tenants_id_fk": { + "name": "helpdesk_contacts_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_contacts_customer_id_customers_id_fk": { + "name": "helpdesk_contacts_customer_id_customers_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_contacts_source_channel_id_helpdesk_channel_instances_id_fk": { + "name": "helpdesk_contacts_source_channel_id_helpdesk_channel_instances_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "helpdesk_channel_instances", + "columnsFrom": [ + "source_channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_contacts_contact_id_contacts_id_fk": { + "name": "helpdesk_contacts_contact_id_contacts_id_fk", + "tableFrom": "helpdesk_contacts", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_conversation_participants": { + "name": "helpdesk_conversation_participants", + "schema": "", + "columns": { + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_conversation_participants_conversation_id_helpdesk_conversations_id_fk": { + "name": "helpdesk_conversation_participants_conversation_id_helpdesk_conversations_id_fk", + "tableFrom": "helpdesk_conversation_participants", + "tableTo": "helpdesk_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversation_participants_user_id_auth_users_id_fk": { + "name": "helpdesk_conversation_participants_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_conversation_participants", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_conversations": { + "name": "helpdesk_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "channel_instance_id": { + "name": "channel_instance_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'normal'" + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "customer_id": { + "name": "customer_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact_person_id": { + "name": "contact_person_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ticket_number": { + "name": "ticket_number", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_conversations_tenant_id_tenants_id_fk": { + "name": "helpdesk_conversations_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversations_channel_instance_id_helpdesk_channel_instances_id_fk": { + "name": "helpdesk_conversations_channel_instance_id_helpdesk_channel_instances_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "helpdesk_channel_instances", + "columnsFrom": [ + "channel_instance_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_conversations_contact_id_helpdesk_contacts_id_fk": { + "name": "helpdesk_conversations_contact_id_helpdesk_contacts_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "helpdesk_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_conversations_assignee_user_id_auth_users_id_fk": { + "name": "helpdesk_conversations_assignee_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "auth_users", + "columnsFrom": [ + "assignee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_conversations_customer_id_customers_id_fk": { + "name": "helpdesk_conversations_customer_id_customers_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "helpdesk_conversations_contact_person_id_contacts_id_fk": { + "name": "helpdesk_conversations_contact_person_id_contacts_id_fk", + "tableFrom": "helpdesk_conversations", + "tableTo": "contacts", + "columnsFrom": [ + "contact_person_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_messages": { + "name": "helpdesk_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_meta": { + "name": "raw_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "external_message_id": { + "name": "external_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_messages_tenant_id_tenants_id_fk": { + "name": "helpdesk_messages_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_messages_conversation_id_helpdesk_conversations_id_fk": { + "name": "helpdesk_messages_conversation_id_helpdesk_conversations_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "helpdesk_conversations", + "columnsFrom": [ + "conversation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_messages_author_user_id_auth_users_id_fk": { + "name": "helpdesk_messages_author_user_id_auth_users_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "auth_users", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "helpdesk_messages_contact_id_helpdesk_contacts_id_fk": { + "name": "helpdesk_messages_contact_id_helpdesk_contacts_id_fk", + "tableFrom": "helpdesk_messages", + "tableTo": "helpdesk_contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "helpdesk_messages_external_message_id_unique": { + "name": "helpdesk_messages_external_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.helpdesk_routing_rules": { + "name": "helpdesk_routing_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "condition": { + "name": "condition", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "helpdesk_routing_rules_tenant_id_tenants_id_fk": { + "name": "helpdesk_routing_rules_tenant_id_tenants_id_fk", + "tableFrom": "helpdesk_routing_rules", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "helpdesk_routing_rules_created_by_auth_users_id_fk": { + "name": "helpdesk_routing_rules_created_by_auth_users_id_fk", + "tableFrom": "helpdesk_routing_rules", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.historyitems": { + "name": "historyitems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "historyitems_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "incomingInvoice": { + "name": "incomingInvoice", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "inventoryitem": { + "name": "inventoryitem", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "product": { + "name": "product", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event": { + "name": "event", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "newVal": { + "name": "newVal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oldVal": { + "name": "oldVal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task": { + "name": "task", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vehicle": { + "name": "vehicle", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bankstatement": { + "name": "bankstatement", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "space": { + "name": "space", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "projecttype": { + "name": "projecttype", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "check": { + "name": "check", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "service": { + "name": "service", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "createddocument": { + "name": "createddocument", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "file": { + "name": "file", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "inventoryitemgroup": { + "name": "inventoryitemgroup", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Software'" + }, + "costcentre": { + "name": "costcentre", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownaccount": { + "name": "ownaccount", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "documentbox": { + "name": "documentbox", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "hourrate": { + "name": "hourrate", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "historyitems_customer_customers_id_fk": { + "name": "historyitems_customer_customers_id_fk", + "tableFrom": "historyitems", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_tenant_tenants_id_fk": { + "name": "historyitems_tenant_tenants_id_fk", + "tableFrom": "historyitems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_vendor_vendors_id_fk": { + "name": "historyitems_vendor_vendors_id_fk", + "tableFrom": "historyitems", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_project_projects_id_fk": { + "name": "historyitems_project_projects_id_fk", + "tableFrom": "historyitems", + "tableTo": "projects", + "columnsFrom": [ + "project" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_plant_plants_id_fk": { + "name": "historyitems_plant_plants_id_fk", + "tableFrom": "historyitems", + "tableTo": "plants", + "columnsFrom": [ + "plant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_incomingInvoice_incominginvoices_id_fk": { + "name": "historyitems_incomingInvoice_incominginvoices_id_fk", + "tableFrom": "historyitems", + "tableTo": "incominginvoices", + "columnsFrom": [ + "incomingInvoice" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_contact_contacts_id_fk": { + "name": "historyitems_contact_contacts_id_fk", + "tableFrom": "historyitems", + "tableTo": "contacts", + "columnsFrom": [ + "contact" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_inventoryitem_inventoryitems_id_fk": { + "name": "historyitems_inventoryitem_inventoryitems_id_fk", + "tableFrom": "historyitems", + "tableTo": "inventoryitems", + "columnsFrom": [ + "inventoryitem" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_product_products_id_fk": { + "name": "historyitems_product_products_id_fk", + "tableFrom": "historyitems", + "tableTo": "products", + "columnsFrom": [ + "product" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "historyitems_event_events_id_fk": { + "name": "historyitems_event_events_id_fk", + "tableFrom": "historyitems", + "tableTo": "events", + "columnsFrom": [ + "event" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_task_tasks_id_fk": { + "name": "historyitems_task_tasks_id_fk", + "tableFrom": "historyitems", + "tableTo": "tasks", + "columnsFrom": [ + "task" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_vehicle_vehicles_id_fk": { + "name": "historyitems_vehicle_vehicles_id_fk", + "tableFrom": "historyitems", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicle" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_bankstatement_bankstatements_id_fk": { + "name": "historyitems_bankstatement_bankstatements_id_fk", + "tableFrom": "historyitems", + "tableTo": "bankstatements", + "columnsFrom": [ + "bankstatement" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_space_spaces_id_fk": { + "name": "historyitems_space_spaces_id_fk", + "tableFrom": "historyitems", + "tableTo": "spaces", + "columnsFrom": [ + "space" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_projecttype_projecttypes_id_fk": { + "name": "historyitems_projecttype_projecttypes_id_fk", + "tableFrom": "historyitems", + "tableTo": "projecttypes", + "columnsFrom": [ + "projecttype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_check_checks_id_fk": { + "name": "historyitems_check_checks_id_fk", + "tableFrom": "historyitems", + "tableTo": "checks", + "columnsFrom": [ + "check" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_service_services_id_fk": { + "name": "historyitems_service_services_id_fk", + "tableFrom": "historyitems", + "tableTo": "services", + "columnsFrom": [ + "service" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_createddocument_createddocuments_id_fk": { + "name": "historyitems_createddocument_createddocuments_id_fk", + "tableFrom": "historyitems", + "tableTo": "createddocuments", + "columnsFrom": [ + "createddocument" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_file_files_id_fk": { + "name": "historyitems_file_files_id_fk", + "tableFrom": "historyitems", + "tableTo": "files", + "columnsFrom": [ + "file" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_inventoryitemgroup_inventoryitemgroups_id_fk": { + "name": "historyitems_inventoryitemgroup_inventoryitemgroups_id_fk", + "tableFrom": "historyitems", + "tableTo": "inventoryitemgroups", + "columnsFrom": [ + "inventoryitemgroup" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_costcentre_costcentres_id_fk": { + "name": "historyitems_costcentre_costcentres_id_fk", + "tableFrom": "historyitems", + "tableTo": "costcentres", + "columnsFrom": [ + "costcentre" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_ownaccount_ownaccounts_id_fk": { + "name": "historyitems_ownaccount_ownaccounts_id_fk", + "tableFrom": "historyitems", + "tableTo": "ownaccounts", + "columnsFrom": [ + "ownaccount" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_documentbox_documentboxes_id_fk": { + "name": "historyitems_documentbox_documentboxes_id_fk", + "tableFrom": "historyitems", + "tableTo": "documentboxes", + "columnsFrom": [ + "documentbox" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_hourrate_hourrates_id_fk": { + "name": "historyitems_hourrate_hourrates_id_fk", + "tableFrom": "historyitems", + "tableTo": "hourrates", + "columnsFrom": [ + "hourrate" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "historyitems_created_by_auth_users_id_fk": { + "name": "historyitems_created_by_auth_users_id_fk", + "tableFrom": "historyitems", + "tableTo": "auth_users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.holidays": { + "name": "holidays", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "holidays_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_code": { + "name": "state_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hourrates": { + "name": "hourrates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hourrates_tenant_tenants_id_fk": { + "name": "hourrates_tenant_tenants_id_fk", + "tableFrom": "hourrates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hourrates_updated_by_auth_users_id_fk": { + "name": "hourrates_updated_by_auth_users_id_fk", + "tableFrom": "hourrates", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.incominginvoices": { + "name": "incominginvoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "incominginvoices_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Entwurf'" + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document": { + "name": "document", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paymentType": { + "name": "paymentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accounts": { + "name": "accounts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[{\"account\":null,\"taxType\":null,\"amountNet\":null,\"amountTax\":19,\"costCentre\":null}]'::jsonb" + }, + "paid": { + "name": "paid", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expense": { + "name": "expense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "incominginvoices_tenant_tenants_id_fk": { + "name": "incominginvoices_tenant_tenants_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incominginvoices_vendor_vendors_id_fk": { + "name": "incominginvoices_vendor_vendors_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "incominginvoices_updated_by_auth_users_id_fk": { + "name": "incominginvoices_updated_by_auth_users_id_fk", + "tableFrom": "incominginvoices", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventoryitemgroups": { + "name": "inventoryitemgroups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inventoryitems": { + "name": "inventoryitems", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "usePlanning": { + "name": "usePlanning", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "inventoryitemgroups_tenant_tenants_id_fk": { + "name": "inventoryitemgroups_tenant_tenants_id_fk", + "tableFrom": "inventoryitemgroups", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitemgroups_updated_by_auth_users_id_fk": { + "name": "inventoryitemgroups_updated_by_auth_users_id_fk", + "tableFrom": "inventoryitemgroups", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inventoryitems": { + "name": "inventoryitems", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "inventoryitems_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usePlanning": { + "name": "usePlanning", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "currentSpace": { + "name": "currentSpace", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "articleNumber": { + "name": "articleNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialNumber": { + "name": "serialNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchaseDate": { + "name": "purchaseDate", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "manufacturer": { + "name": "manufacturer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manufacturerNumber": { + "name": "manufacturerNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currentValue": { + "name": "currentValue", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "inventoryitems_tenant_tenants_id_fk": { + "name": "inventoryitems_tenant_tenants_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_currentSpace_spaces_id_fk": { + "name": "inventoryitems_currentSpace_spaces_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "spaces", + "columnsFrom": [ + "currentSpace" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_vendor_vendors_id_fk": { + "name": "inventoryitems_vendor_vendors_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "inventoryitems_updated_by_auth_users_id_fk": { + "name": "inventoryitems_updated_by_auth_users_id_fk", + "tableFrom": "inventoryitems", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.letterheads": { + "name": "letterheads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "letterheads_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Standard'" + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentTypes": { + "name": "documentTypes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "letterheads_tenant_tenants_id_fk": { + "name": "letterheads_tenant_tenants_id_fk", + "tableFrom": "letterheads", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "letterheads_updated_by_auth_users_id_fk": { + "name": "letterheads_updated_by_auth_users_id_fk", + "tableFrom": "letterheads", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.movements": { + "name": "movements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "movements_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "productId": { + "name": "productId", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spaceId": { + "name": "spaceId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "projectId": { + "name": "projectId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serials": { + "name": "serials", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "movements_productId_products_id_fk": { + "name": "movements_productId_products_id_fk", + "tableFrom": "movements", + "tableTo": "products", + "columnsFrom": [ + "productId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_spaceId_spaces_id_fk": { + "name": "movements_spaceId_spaces_id_fk", + "tableFrom": "movements", + "tableTo": "spaces", + "columnsFrom": [ + "spaceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_tenant_tenants_id_fk": { + "name": "movements_tenant_tenants_id_fk", + "tableFrom": "movements", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_projectId_projects_id_fk": { + "name": "movements_projectId_projects_id_fk", + "tableFrom": "movements", + "tableTo": "projects", + "columnsFrom": [ + "projectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "movements_updated_by_auth_users_id_fk": { + "name": "movements_updated_by_auth_users_id_fk", + "tableFrom": "movements", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_event_types": { + "name": "notifications_event_types", + "schema": "", + "columns": { + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "notification_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "allowed_channels": { + "name": "allowed_channels", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"inapp\",\"email\"]'::jsonb" + }, + "payload_schema": { + "name": "payload_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_items": { + "name": "notifications_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_items_tenant_id_tenants_id_fk": { + "name": "notifications_items_tenant_id_tenants_id_fk", + "tableFrom": "notifications_items", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_items_user_id_auth_users_id_fk": { + "name": "notifications_items_user_id_auth_users_id_fk", + "tableFrom": "notifications_items", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_items_event_type_notifications_event_types_event_key_fk": { + "name": "notifications_items_event_type_notifications_event_types_event_key_fk", + "tableFrom": "notifications_items", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_type" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_preferences": { + "name": "notifications_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_preferences_tenant_id_user_id_event_type_chan_key": { + "name": "notifications_preferences_tenant_id_user_id_event_type_chan_key", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_preferences_tenant_id_tenants_id_fk": { + "name": "notifications_preferences_tenant_id_tenants_id_fk", + "tableFrom": "notifications_preferences", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_user_id_auth_users_id_fk": { + "name": "notifications_preferences_user_id_auth_users_id_fk", + "tableFrom": "notifications_preferences", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_event_type_notifications_event_types_event_key_fk": { + "name": "notifications_preferences_event_type_notifications_event_types_event_key_fk", + "tableFrom": "notifications_preferences", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_type" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications_preferences_defaults": { + "name": "notifications_preferences_defaults", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notifications_preferences_defau_tenant_id_event_key_channel_key": { + "name": "notifications_preferences_defau_tenant_id_event_key_channel_key", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notifications_preferences_defaults_tenant_id_tenants_id_fk": { + "name": "notifications_preferences_defaults_tenant_id_tenants_id_fk", + "tableFrom": "notifications_preferences_defaults", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "notifications_preferences_defaults_event_key_notifications_event_types_event_key_fk": { + "name": "notifications_preferences_defaults_event_key_notifications_event_types_event_key_fk", + "tableFrom": "notifications_preferences_defaults", + "tableTo": "notifications_event_types", + "columnsFrom": [ + "event_key" + ], + "columnsTo": [ + "event_key" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ownaccounts": { + "name": "ownaccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "ownaccounts_tenant_tenants_id_fk": { + "name": "ownaccounts_tenant_tenants_id_fk", + "tableFrom": "ownaccounts", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "ownaccounts_updated_by_auth_users_id_fk": { + "name": "ownaccounts_updated_by_auth_users_id_fk", + "tableFrom": "ownaccounts", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plants": { + "name": "plants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "plants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"html\":\"\",\"json\":[],\"text\":\"\"}'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "plants_tenant_tenants_id_fk": { + "name": "plants_tenant_tenants_id_fk", + "tableFrom": "plants", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_customer_customers_id_fk": { + "name": "plants_customer_customers_id_fk", + "tableFrom": "plants", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_contract_contracts_id_fk": { + "name": "plants_contract_contracts_id_fk", + "tableFrom": "plants", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "plants_updated_by_auth_users_id_fk": { + "name": "plants_updated_by_auth_users_id_fk", + "tableFrom": "plants", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.productcategories": { + "name": "productcategories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "productcategories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "productcategories_tenant_tenants_id_fk": { + "name": "productcategories_tenant_tenants_id_fk", + "tableFrom": "productcategories", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "productcategories_updated_by_auth_users_id_fk": { + "name": "productcategories_updated_by_auth_users_id_fk", + "tableFrom": "productcategories", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "products_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manufacturer": { + "name": "manufacturer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ean": { + "name": "ean", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "barcode": { + "name": "barcode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purchasePrice": { + "name": "purchasePrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manufacturerNumber": { + "name": "manufacturerNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vendorAllocation": { + "name": "vendorAllocation", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "articleNumber": { + "name": "articleNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "barcodes": { + "name": "barcodes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "productcategories": { + "name": "productcategories", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "taxPercentage": { + "name": "taxPercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 19 + }, + "markupPercentage": { + "name": "markupPercentage", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_unit_units_id_fk": { + "name": "products_unit_units_id_fk", + "tableFrom": "products", + "tableTo": "units", + "columnsFrom": [ + "unit" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_tenant_tenants_id_fk": { + "name": "products_tenant_tenants_id_fk", + "tableFrom": "products", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_updated_by_auth_users_id_fk": { + "name": "products_updated_by_auth_users_id_fk", + "tableFrom": "products", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "projects_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "phases": { + "name": "phases", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "description": { + "name": "description", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "forms": { + "name": "forms", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "heroId": { + "name": "heroId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "measure": { + "name": "measure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "projectNumber": { + "name": "projectNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract": { + "name": "contract", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "projectType": { + "name": "projectType", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'Projekt'" + }, + "projecttype": { + "name": "projecttype", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customerRef": { + "name": "customerRef", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_phase": { + "name": "active_phase", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projects_tenant_tenants_id_fk": { + "name": "projects_tenant_tenants_id_fk", + "tableFrom": "projects", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_customer_customers_id_fk": { + "name": "projects_customer_customers_id_fk", + "tableFrom": "projects", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_contract_contracts_id_fk": { + "name": "projects_contract_contracts_id_fk", + "tableFrom": "projects", + "tableTo": "contracts", + "columnsFrom": [ + "contract" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_projecttype_projecttypes_id_fk": { + "name": "projects_projecttype_projecttypes_id_fk", + "tableFrom": "projects", + "tableTo": "projecttypes", + "columnsFrom": [ + "projecttype" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_updated_by_auth_users_id_fk": { + "name": "projects_updated_by_auth_users_id_fk", + "tableFrom": "projects", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projecttypes": { + "name": "projecttypes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "projecttypes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initialPhases": { + "name": "initialPhases", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "addablePhases": { + "name": "addablePhases", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "projecttypes_tenant_tenants_id_fk": { + "name": "projecttypes_tenant_tenants_id_fk", + "tableFrom": "projecttypes", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projecttypes_updated_by_auth_users_id_fk": { + "name": "projecttypes_updated_by_auth_users_id_fk", + "tableFrom": "projecttypes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.servicecategories": { + "name": "servicecategories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "servicecategories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discount": { + "name": "discount", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "servicecategories_tenant_tenants_id_fk": { + "name": "servicecategories_tenant_tenants_id_fk", + "tableFrom": "servicecategories", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "servicecategories_updated_by_auth_users_id_fk": { + "name": "servicecategories_updated_by_auth_users_id_fk", + "tableFrom": "servicecategories", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.services": { + "name": "services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "services_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sellingPrice": { + "name": "sellingPrice", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "serviceNumber": { + "name": "serviceNumber", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "servicecategories": { + "name": "servicecategories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "purchasePriceComposed": { + "name": "purchasePriceComposed", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"total\":0}'::jsonb" + }, + "sellingPriceComposed": { + "name": "sellingPriceComposed", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"total\":0}'::jsonb" + }, + "taxPercentage": { + "name": "taxPercentage", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 19 + }, + "materialComposition": { + "name": "materialComposition", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "personalComposition": { + "name": "personalComposition", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "services_tenant_tenants_id_fk": { + "name": "services_tenant_tenants_id_fk", + "tableFrom": "services", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_unit_units_id_fk": { + "name": "services_unit_units_id_fk", + "tableFrom": "services", + "tableTo": "units", + "columnsFrom": [ + "unit" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "services_updated_by_auth_users_id_fk": { + "name": "services_updated_by_auth_users_id_fk", + "tableFrom": "services", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spaces": { + "name": "spaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "spaces_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "spaceNumber": { + "name": "spaceNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parentSpace": { + "name": "parentSpace", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"zip\":\"\",\"city\":\"\",\"streetNumber\":\"\"}'::jsonb" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "spaces_tenant_tenants_id_fk": { + "name": "spaces_tenant_tenants_id_fk", + "tableFrom": "spaces", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spaces_parentSpace_spaces_id_fk": { + "name": "spaces_parentSpace_spaces_id_fk", + "tableFrom": "spaces", + "tableTo": "spaces", + "columnsFrom": [ + "parentSpace" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "spaces_updated_by_auth_users_id_fk": { + "name": "spaces_updated_by_auth_users_id_fk", + "tableFrom": "spaces", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_time_entries": { + "name": "staff_time_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE \n WHEN stopped_at IS NOT NULL \n THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)\n ELSE NULL\n END", + "type": "stored" + } + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'work'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "times_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "device": { + "name": "device", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "internal_note": { + "name": "internal_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vacation_reason": { + "name": "vacation_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vacation_days": { + "name": "vacation_days", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sick_reason": { + "name": "sick_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "staff_time_entries_tenant_id_tenants_id_fk": { + "name": "staff_time_entries_tenant_id_tenants_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_time_entries_user_id_auth_users_id_fk": { + "name": "staff_time_entries_user_id_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "staff_time_entries_updated_by_auth_users_id_fk": { + "name": "staff_time_entries_updated_by_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_time_entries_approved_by_auth_users_id_fk": { + "name": "staff_time_entries_approved_by_auth_users_id_fk", + "tableFrom": "staff_time_entries", + "tableTo": "auth_users", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_time_entry_connects": { + "name": "staff_time_entry_connects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "time_entry_id": { + "name": "time_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)", + "type": "stored" + } + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "staff_time_entry_connects_time_entry_id_staff_time_entries_id_fk": { + "name": "staff_time_entry_connects_time_entry_id_staff_time_entries_id_fk", + "tableFrom": "staff_time_entry_connects", + "tableTo": "staff_time_entries", + "columnsFrom": [ + "time_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_zeitstromtimestamps": { + "name": "staff_zeitstromtimestamps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "intent": { + "name": "intent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "staff_time_entry": { + "name": "staff_time_entry", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "internal_note": { + "name": "internal_note", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "staff_zeitstromtimestamps_tenant_tenants_id_fk": { + "name": "staff_zeitstromtimestamps_tenant_tenants_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_zeitstromtimestamps_profile_auth_profiles_id_fk": { + "name": "staff_zeitstromtimestamps_profile_auth_profiles_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "auth_profiles", + "columnsFrom": [ + "profile" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_zeitstromtimestamps_staff_time_entry_staff_time_entries_id_fk": { + "name": "staff_zeitstromtimestamps_staff_time_entry_staff_time_entries_id_fk", + "tableFrom": "staff_zeitstromtimestamps", + "tableTo": "staff_time_entries", + "columnsFrom": [ + "staff_time_entry" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.statementallocations": { + "name": "statementallocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bs_id": { + "name": "bs_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cd_id": { + "name": "cd_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ii_id": { + "name": "ii_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "account": { + "name": "account", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "ownaccount": { + "name": "ownaccount", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "vendor": { + "name": "vendor", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "statementallocations_bs_id_bankstatements_id_fk": { + "name": "statementallocations_bs_id_bankstatements_id_fk", + "tableFrom": "statementallocations", + "tableTo": "bankstatements", + "columnsFrom": [ + "bs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_cd_id_createddocuments_id_fk": { + "name": "statementallocations_cd_id_createddocuments_id_fk", + "tableFrom": "statementallocations", + "tableTo": "createddocuments", + "columnsFrom": [ + "cd_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_ii_id_incominginvoices_id_fk": { + "name": "statementallocations_ii_id_incominginvoices_id_fk", + "tableFrom": "statementallocations", + "tableTo": "incominginvoices", + "columnsFrom": [ + "ii_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_tenant_tenants_id_fk": { + "name": "statementallocations_tenant_tenants_id_fk", + "tableFrom": "statementallocations", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_account_accounts_id_fk": { + "name": "statementallocations_account_accounts_id_fk", + "tableFrom": "statementallocations", + "tableTo": "accounts", + "columnsFrom": [ + "account" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_ownaccount_ownaccounts_id_fk": { + "name": "statementallocations_ownaccount_ownaccounts_id_fk", + "tableFrom": "statementallocations", + "tableTo": "ownaccounts", + "columnsFrom": [ + "ownaccount" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_customer_customers_id_fk": { + "name": "statementallocations_customer_customers_id_fk", + "tableFrom": "statementallocations", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_vendor_vendors_id_fk": { + "name": "statementallocations_vendor_vendors_id_fk", + "tableFrom": "statementallocations", + "tableTo": "vendors", + "columnsFrom": [ + "vendor" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "statementallocations_updated_by_auth_users_id_fk": { + "name": "statementallocations_updated_by_auth_users_id_fk", + "tableFrom": "statementallocations", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tasks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categorie": { + "name": "categorie", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project": { + "name": "project", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plant": { + "name": "plant", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer": { + "name": "customer", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tasks_tenant_tenants_id_fk": { + "name": "tasks_tenant_tenants_id_fk", + "tableFrom": "tasks", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_user_id_auth_users_id_fk": { + "name": "tasks_user_id_auth_users_id_fk", + "tableFrom": "tasks", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_customer_customers_id_fk": { + "name": "tasks_customer_customers_id_fk", + "tableFrom": "tasks", + "tableTo": "customers", + "columnsFrom": [ + "customer" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_updated_by_auth_users_id_fk": { + "name": "tasks_updated_by_auth_users_id_fk", + "tableFrom": "tasks", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.taxtypes": { + "name": "taxtypes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "taxtypes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "percentage": { + "name": "percentage", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "taxtypes_updated_by_auth_users_id_fk": { + "name": "taxtypes_updated_by_auth_users_id_fk", + "tableFrom": "taxtypes", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tenants": { + "name": "tenants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "tenants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendarConfig": { + "name": "calendarConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"eventTypes\":[{\"color\":\"blue\",\"label\":\"Büro\"},{\"color\":\"yellow\",\"label\":\"Besprechung\"},{\"color\":\"green\",\"label\":\"Umsetzung\"},{\"color\":\"red\",\"label\":\"Vor Ort Termin\"}]}'::jsonb" + }, + "timeConfig": { + "name": "timeConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"products\":[],\"documents\":[]}'::jsonb" + }, + "measures": { + "name": "measures", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "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\"}]'::jsonb" + }, + "businessInfo": { + "name": "businessInfo", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"zip\":\"\",\"city\":\"\",\"name\":\"\",\"street\":\"\"}'::jsonb" + }, + "features": { + "name": "features", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{\"objects\":true,\"calendar\":true,\"contacts\":true,\"projects\":true,\"vehicles\":true,\"contracts\":true,\"inventory\":true,\"accounting\":true,\"timeTracking\":true,\"planningBoard\":true,\"workingTimeTracking\":true}'::jsonb" + }, + "ownFields": { + "name": "ownFields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "numberRanges": { + "name": "numberRanges", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "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}}'::jsonb" + }, + "standardEmailForInvoices": { + "name": "standardEmailForInvoices", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extraModules": { + "name": "extraModules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "isInTrial": { + "name": "isInTrial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "trialEndDate": { + "name": "trialEndDate", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "stripeCustomerId": { + "name": "stripeCustomerId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasActiveLicense": { + "name": "hasActiveLicense", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "userLicenseCount": { + "name": "userLicenseCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "workstationLicenseCount": { + "name": "workstationLicenseCount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "standardPaymentDays": { + "name": "standardPaymentDays", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 14 + }, + "dokuboxEmailAddresses": { + "name": "dokuboxEmailAddresses", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "dokuboxkey": { + "name": "dokuboxkey", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "autoPrepareIncomingInvoices": { + "name": "autoPrepareIncomingInvoices", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "portalDomain": { + "name": "portalDomain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "portalConfig": { + "name": "portalConfig", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"primayColor\":\"#69c350\"}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "locked_tenant", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "tenants_updated_by_auth_users_id_fk": { + "name": "tenants_updated_by_auth_users_id_fk", + "tableFrom": "tenants", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.texttemplates": { + "name": "texttemplates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "texttemplates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "documentType": { + "name": "documentType", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "default": { + "name": "default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "pos": { + "name": "pos", + "type": "texttemplatepositions", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "texttemplates_tenant_tenants_id_fk": { + "name": "texttemplates_tenant_tenants_id_fk", + "tableFrom": "texttemplates", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "texttemplates_updated_by_auth_users_id_fk": { + "name": "texttemplates_updated_by_auth_users_id_fk", + "tableFrom": "texttemplates", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.units": { + "name": "units", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "units_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "single": { + "name": "single", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "multiple": { + "name": "multiple", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_credentials": { + "name": "user_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "smtp_port": { + "name": "smtp_port", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "smtp_ssl": { + "name": "smtp_ssl", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "imap_port": { + "name": "imap_port", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "imap_ssl": { + "name": "imap_ssl", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email_encrypted": { + "name": "email_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "password_encrypted": { + "name": "password_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "smtp_host_encrypted": { + "name": "smtp_host_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "imap_host_encrypted": { + "name": "imap_host_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "access_token_encrypted": { + "name": "access_token_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "refresh_token_encrypted": { + "name": "refresh_token_encrypted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_credentials_user_id_auth_users_id_fk": { + "name": "user_credentials_user_id_auth_users_id_fk", + "tableFrom": "user_credentials", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_credentials_tenant_id_tenants_id_fk": { + "name": "user_credentials_tenant_id_tenants_id_fk", + "tableFrom": "user_credentials", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles": { + "name": "vehicles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "vehicles_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "licensePlate": { + "name": "licensePlate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "driver": { + "name": "driver", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "vin": { + "name": "vin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tankSize": { + "name": "tankSize", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "buildYear": { + "name": "buildYear", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "towingCapacity": { + "name": "towingCapacity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "powerInKW": { + "name": "powerInKW", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "vehicles_tenant_tenants_id_fk": { + "name": "vehicles_tenant_tenants_id_fk", + "tableFrom": "vehicles", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vehicles_driver_auth_users_id_fk": { + "name": "vehicles_driver_auth_users_id_fk", + "tableFrom": "vehicles", + "tableTo": "auth_users", + "columnsFrom": [ + "driver" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vehicles_updated_by_auth_users_id_fk": { + "name": "vehicles_updated_by_auth_users_id_fk", + "tableFrom": "vehicles", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors": { + "name": "vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "vendors_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vendorNumber": { + "name": "vendorNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "infoData": { + "name": "infoData", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hasSEPA": { + "name": "hasSEPA", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "profiles": { + "name": "profiles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "defaultPaymentMethod": { + "name": "defaultPaymentMethod", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "vendors_tenant_tenants_id_fk": { + "name": "vendors_tenant_tenants_id_fk", + "tableFrom": "vendors", + "tableTo": "tenants", + "columnsFrom": [ + "tenant" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vendors_updated_by_auth_users_id_fk": { + "name": "vendors_updated_by_auth_users_id_fk", + "tableFrom": "vendors", + "tableTo": "auth_users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.time_events": { + "name": "time_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tenant_id": { + "name": "tenant_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_time": { + "name": "event_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invalidates_event_id": { + "name": "invalidates_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_time_events_tenant_user_time": { + "name": "idx_time_events_tenant_user_time", + "columns": [ + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_time_events_created_at": { + "name": "idx_time_events_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_time_events_invalidates": { + "name": "idx_time_events_invalidates", + "columns": [ + { + "expression": "invalidates_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "time_events_tenant_id_tenants_id_fk": { + "name": "time_events_tenant_id_tenants_id_fk", + "tableFrom": "time_events", + "tableTo": "tenants", + "columnsFrom": [ + "tenant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_events_user_id_auth_users_id_fk": { + "name": "time_events_user_id_auth_users_id_fk", + "tableFrom": "time_events", + "tableTo": "auth_users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_events_actor_user_id_auth_users_id_fk": { + "name": "time_events_actor_user_id_auth_users_id_fk", + "tableFrom": "time_events", + "tableTo": "auth_users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "time_events_invalidates_event_id_time_events_id_fk": { + "name": "time_events_invalidates_event_id_time_events_id_fk", + "tableFrom": "time_events", + "tableTo": "time_events", + "columnsFrom": [ + "invalidates_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "time_events_actor_user_check": { + "name": "time_events_actor_user_check", + "value": "\n (actor_type = 'system' AND actor_user_id IS NULL)\n OR\n (actor_type = 'user' AND actor_user_id IS NOT NULL)\n " + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.credential_types": { + "name": "credential_types", + "schema": "public", + "values": [ + "mail", + "m365" + ] + }, + "public.folderfunctions": { + "name": "folderfunctions", + "schema": "public", + "values": [ + "none", + "yearSubCategory", + "incomingInvoices", + "invoices", + "quotes", + "confirmationOrders", + "deliveryNotes", + "vehicleData", + "reminders", + "taxData", + "deposit", + "timeEvaluations" + ] + }, + "public.locked_tenant": { + "name": "locked_tenant", + "schema": "public", + "values": [ + "maintenance_tenant", + "maintenance", + "general", + "no_subscription" + ] + }, + "public.notification_channel": { + "name": "notification_channel", + "schema": "public", + "values": [ + "email", + "inapp", + "sms", + "push", + "webhook" + ] + }, + "public.notification_severity": { + "name": "notification_severity", + "schema": "public", + "values": [ + "info", + "success", + "warning", + "error" + ] + }, + "public.notification_status": { + "name": "notification_status", + "schema": "public", + "values": [ + "queued", + "sent", + "failed", + "read" + ] + }, + "public.payment_types": { + "name": "payment_types", + "schema": "public", + "values": [ + "transfer", + "direct_debit" + ] + }, + "public.texttemplatepositions": { + "name": "texttemplatepositions", + "schema": "public", + "values": [ + "startText", + "endText" + ] + }, + "public.times_state": { + "name": "times_state", + "schema": "public", + "values": [ + "submitted", + "approved", + "draft" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json new file mode 100644 index 0000000..25a83ac --- /dev/null +++ b/db/migrations/meta/_journal.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/db/schema/accounts.ts b/db/schema/accounts.ts new file mode 100644 index 0000000..cc154de --- /dev/null +++ b/db/schema/accounts.ts @@ -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 diff --git a/db/schema/auth_profiles.ts b/db/schema/auth_profiles.ts new file mode 100644 index 0000000..85085e0 --- /dev/null +++ b/db/schema/auth_profiles.ts @@ -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("

Mit freundlichen Grüßen

"), + + 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 diff --git a/db/schema/auth_role_permisssions.ts b/db/schema/auth_role_permisssions.ts new file mode 100644 index 0000000..fafba53 --- /dev/null +++ b/db/schema/auth_role_permisssions.ts @@ -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 diff --git a/db/schema/auth_roles.ts b/db/schema/auth_roles.ts new file mode 100644 index 0000000..2f8f657 --- /dev/null +++ b/db/schema/auth_roles.ts @@ -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 diff --git a/db/schema/auth_tenant_users.ts b/db/schema/auth_tenant_users.ts new file mode 100644 index 0000000..4bf5709 --- /dev/null +++ b/db/schema/auth_tenant_users.ts @@ -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 diff --git a/db/schema/auth_user_roles.ts b/db/schema/auth_user_roles.ts new file mode 100644 index 0000000..4bec35e --- /dev/null +++ b/db/schema/auth_user_roles.ts @@ -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 diff --git a/db/schema/auth_users.ts b/db/schema/auth_users.ts new file mode 100644 index 0000000..224bd74 --- /dev/null +++ b/db/schema/auth_users.ts @@ -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 diff --git a/db/schema/bankaccounts.ts b/db/schema/bankaccounts.ts new file mode 100644 index 0000000..2b431d4 --- /dev/null +++ b/db/schema/bankaccounts.ts @@ -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 diff --git a/db/schema/bankrequisitions.ts b/db/schema/bankrequisitions.ts new file mode 100644 index 0000000..3cfd18e --- /dev/null +++ b/db/schema/bankrequisitions.ts @@ -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 diff --git a/db/schema/bankstatements.ts b/db/schema/bankstatements.ts new file mode 100644 index 0000000..c69ee2e --- /dev/null +++ b/db/schema/bankstatements.ts @@ -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 diff --git a/db/schema/checkexecutions.ts b/db/schema/checkexecutions.ts new file mode 100644 index 0000000..455877c --- /dev/null +++ b/db/schema/checkexecutions.ts @@ -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 diff --git a/db/schema/checks.ts b/db/schema/checks.ts new file mode 100644 index 0000000..45f2a26 --- /dev/null +++ b/db/schema/checks.ts @@ -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 diff --git a/db/schema/citys.ts b/db/schema/citys.ts new file mode 100644 index 0000000..e54e5fd --- /dev/null +++ b/db/schema/citys.ts @@ -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 diff --git a/db/schema/contacts.ts b/db/schema/contacts.ts new file mode 100644 index 0000000..b3f3824 --- /dev/null +++ b/db/schema/contacts.ts @@ -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 diff --git a/db/schema/contracts.ts b/db/schema/contracts.ts new file mode 100644 index 0000000..3673395 --- /dev/null +++ b/db/schema/contracts.ts @@ -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 diff --git a/db/schema/costcentres.ts b/db/schema/costcentres.ts new file mode 100644 index 0000000..7dccb10 --- /dev/null +++ b/db/schema/costcentres.ts @@ -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 diff --git a/db/schema/countrys.ts b/db/schema/countrys.ts new file mode 100644 index 0000000..9eb3e9f --- /dev/null +++ b/db/schema/countrys.ts @@ -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 diff --git a/db/schema/createddocuments.ts b/db/schema/createddocuments.ts new file mode 100644 index 0000000..9fd881a --- /dev/null +++ b/db/schema/createddocuments.ts @@ -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 diff --git a/db/schema/createdletters.ts b/db/schema/createdletters.ts new file mode 100644 index 0000000..a180624 --- /dev/null +++ b/db/schema/createdletters.ts @@ -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 diff --git a/db/schema/customers.ts b/db/schema/customers.ts new file mode 100644 index 0000000..67743e8 --- /dev/null +++ b/db/schema/customers.ts @@ -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 diff --git a/db/schema/devices.ts b/db/schema/devices.ts new file mode 100644 index 0000000..4825826 --- /dev/null +++ b/db/schema/devices.ts @@ -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 diff --git a/db/schema/documentboxes.ts b/db/schema/documentboxes.ts new file mode 100644 index 0000000..1111a19 --- /dev/null +++ b/db/schema/documentboxes.ts @@ -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 diff --git a/db/schema/enums.ts b/db/schema/enums.ts new file mode 100644 index 0000000..3030302 --- /dev/null +++ b/db/schema/enums.ts @@ -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 + diff --git a/db/schema/events.ts b/db/schema/events.ts new file mode 100644 index 0000000..b703c3e --- /dev/null +++ b/db/schema/events.ts @@ -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 diff --git a/db/schema/files.ts b/db/schema/files.ts new file mode 100644 index 0000000..a151421 --- /dev/null +++ b/db/schema/files.ts @@ -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 diff --git a/db/schema/filetags.ts b/db/schema/filetags.ts new file mode 100644 index 0000000..3dc47cf --- /dev/null +++ b/db/schema/filetags.ts @@ -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 diff --git a/db/schema/folders.ts b/db/schema/folders.ts new file mode 100644 index 0000000..74a9a11 --- /dev/null +++ b/db/schema/folders.ts @@ -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 diff --git a/db/schema/generatedexports.ts b/db/schema/generatedexports.ts new file mode 100644 index 0000000..b2a5c84 --- /dev/null +++ b/db/schema/generatedexports.ts @@ -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 diff --git a/db/schema/globalmessages.ts b/db/schema/globalmessages.ts new file mode 100644 index 0000000..3a2ffb9 --- /dev/null +++ b/db/schema/globalmessages.ts @@ -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 diff --git a/db/schema/globalmessagesseen.ts b/db/schema/globalmessagesseen.ts new file mode 100644 index 0000000..7ee9c8e --- /dev/null +++ b/db/schema/globalmessagesseen.ts @@ -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(), +}) diff --git a/db/schema/helpdesk_channel_instances.ts b/db/schema/helpdesk_channel_instances.ts new file mode 100644 index 0000000..29edfea --- /dev/null +++ b/db/schema/helpdesk_channel_instances.ts @@ -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 diff --git a/db/schema/helpdesk_channel_types.ts b/db/schema/helpdesk_channel_types.ts new file mode 100644 index 0000000..8b1a8f3 --- /dev/null +++ b/db/schema/helpdesk_channel_types.ts @@ -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 diff --git a/db/schema/helpdesk_contacts.ts b/db/schema/helpdesk_contacts.ts new file mode 100644 index 0000000..baeab18 --- /dev/null +++ b/db/schema/helpdesk_contacts.ts @@ -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 diff --git a/db/schema/helpdesk_conversation_participants.ts b/db/schema/helpdesk_conversation_participants.ts new file mode 100644 index 0000000..c4d7c6b --- /dev/null +++ b/db/schema/helpdesk_conversation_participants.ts @@ -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 diff --git a/db/schema/helpdesk_conversations.ts b/db/schema/helpdesk_conversations.ts new file mode 100644 index 0000000..f2c14dd --- /dev/null +++ b/db/schema/helpdesk_conversations.ts @@ -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 diff --git a/db/schema/helpdesk_messages.ts b/db/schema/helpdesk_messages.ts new file mode 100644 index 0000000..08eec71 --- /dev/null +++ b/db/schema/helpdesk_messages.ts @@ -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 diff --git a/db/schema/helpdesk_routing_rules.ts b/db/schema/helpdesk_routing_rules.ts new file mode 100644 index 0000000..4c3065b --- /dev/null +++ b/db/schema/helpdesk_routing_rules.ts @@ -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 diff --git a/db/schema/historyitems.ts b/db/schema/historyitems.ts new file mode 100644 index 0000000..4f82235 --- /dev/null +++ b/db/schema/historyitems.ts @@ -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 diff --git a/db/schema/holidays.ts b/db/schema/holidays.ts new file mode 100644 index 0000000..9a53dd8 --- /dev/null +++ b/db/schema/holidays.ts @@ -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 diff --git a/db/schema/hourrates.ts b/db/schema/hourrates.ts new file mode 100644 index 0000000..299c2d5 --- /dev/null +++ b/db/schema/hourrates.ts @@ -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 diff --git a/db/schema/incominginvoices.ts b/db/schema/incominginvoices.ts new file mode 100644 index 0000000..07ac711 --- /dev/null +++ b/db/schema/incominginvoices.ts @@ -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 diff --git a/db/schema/index.ts b/db/schema/index.ts new file mode 100644 index 0000000..47d8bee --- /dev/null +++ b/db/schema/index.ts @@ -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" \ No newline at end of file diff --git a/db/schema/inventoryitemgroups.ts b/db/schema/inventoryitemgroups.ts new file mode 100644 index 0000000..3fe9dc1 --- /dev/null +++ b/db/schema/inventoryitemgroups.ts @@ -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 diff --git a/db/schema/inventoryitems.ts b/db/schema/inventoryitems.ts new file mode 100644 index 0000000..f3437d8 --- /dev/null +++ b/db/schema/inventoryitems.ts @@ -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 diff --git a/db/schema/letterheads.ts b/db/schema/letterheads.ts new file mode 100644 index 0000000..2cbc534 --- /dev/null +++ b/db/schema/letterheads.ts @@ -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 diff --git a/db/schema/movements.ts b/db/schema/movements.ts new file mode 100644 index 0000000..72a2759 --- /dev/null +++ b/db/schema/movements.ts @@ -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 diff --git a/db/schema/notifications_event_types.ts b/db/schema/notifications_event_types.ts new file mode 100644 index 0000000..c16d648 --- /dev/null +++ b/db/schema/notifications_event_types.ts @@ -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 diff --git a/db/schema/notifications_items.ts b/db/schema/notifications_items.ts new file mode 100644 index 0000000..d6c6cb3 --- /dev/null +++ b/db/schema/notifications_items.ts @@ -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 diff --git a/db/schema/notifications_preferences.ts b/db/schema/notifications_preferences.ts new file mode 100644 index 0000000..8397a0a --- /dev/null +++ b/db/schema/notifications_preferences.ts @@ -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 diff --git a/db/schema/notifications_preferences_defaults.ts b/db/schema/notifications_preferences_defaults.ts new file mode 100644 index 0000000..3c263c9 --- /dev/null +++ b/db/schema/notifications_preferences_defaults.ts @@ -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 diff --git a/db/schema/ownaccounts.ts b/db/schema/ownaccounts.ts new file mode 100644 index 0000000..1970adc --- /dev/null +++ b/db/schema/ownaccounts.ts @@ -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 diff --git a/db/schema/plants.ts b/db/schema/plants.ts new file mode 100644 index 0000000..a7b35f4 --- /dev/null +++ b/db/schema/plants.ts @@ -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 diff --git a/db/schema/productcategories.ts b/db/schema/productcategories.ts new file mode 100644 index 0000000..980a7fc --- /dev/null +++ b/db/schema/productcategories.ts @@ -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 diff --git a/db/schema/products.ts b/db/schema/products.ts new file mode 100644 index 0000000..d070254 --- /dev/null +++ b/db/schema/products.ts @@ -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 diff --git a/db/schema/projects.ts b/db/schema/projects.ts new file mode 100644 index 0000000..badf583 --- /dev/null +++ b/db/schema/projects.ts @@ -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 diff --git a/db/schema/projecttypes.ts b/db/schema/projecttypes.ts new file mode 100644 index 0000000..fc7b90d --- /dev/null +++ b/db/schema/projecttypes.ts @@ -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 diff --git a/db/schema/public_links.ts b/db/schema/public_links.ts new file mode 100644 index 0000000..283f96b --- /dev/null +++ b/db/schema/public_links.ts @@ -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(), +}); \ No newline at end of file diff --git a/db/schema/serialexecutions.ts b/db/schema/serialexecutions.ts new file mode 100644 index 0000000..c63cfb2 --- /dev/null +++ b/db/schema/serialexecutions.ts @@ -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" +}); \ No newline at end of file diff --git a/db/schema/serialtypes.ts b/db/schema/serialtypes.ts new file mode 100644 index 0000000..6a32d3b --- /dev/null +++ b/db/schema/serialtypes.ts @@ -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 diff --git a/db/schema/servicecategories.ts b/db/schema/servicecategories.ts new file mode 100644 index 0000000..f0364b5 --- /dev/null +++ b/db/schema/servicecategories.ts @@ -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 diff --git a/db/schema/services.ts b/db/schema/services.ts new file mode 100644 index 0000000..0dcdd4b --- /dev/null +++ b/db/schema/services.ts @@ -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 diff --git a/db/schema/spaces.ts b/db/schema/spaces.ts new file mode 100644 index 0000000..7b8ce0d --- /dev/null +++ b/db/schema/spaces.ts @@ -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 diff --git a/db/schema/staff_time_entries.ts b/db/schema/staff_time_entries.ts new file mode 100644 index 0000000..5159aa0 --- /dev/null +++ b/db/schema/staff_time_entries.ts @@ -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 diff --git a/db/schema/staff_time_entry_connects.ts b/db/schema/staff_time_entry_connects.ts new file mode 100644 index 0000000..dbdd3a3 --- /dev/null +++ b/db/schema/staff_time_entry_connects.ts @@ -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 diff --git a/db/schema/staff_time_events.ts b/db/schema/staff_time_events.ts new file mode 100644 index 0000000..d58f77c --- /dev/null +++ b/db/schema/staff_time_events.ts @@ -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) + ` + ), + }) +); diff --git a/db/schema/staff_zeitstromtimestamps.ts b/db/schema/staff_zeitstromtimestamps.ts new file mode 100644 index 0000000..174e843 --- /dev/null +++ b/db/schema/staff_zeitstromtimestamps.ts @@ -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 diff --git a/db/schema/statementallocations.ts b/db/schema/statementallocations.ts new file mode 100644 index 0000000..c0db9bc --- /dev/null +++ b/db/schema/statementallocations.ts @@ -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 diff --git a/db/schema/tasks.ts b/db/schema/tasks.ts new file mode 100644 index 0000000..aa40617 --- /dev/null +++ b/db/schema/tasks.ts @@ -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 diff --git a/db/schema/taxtypes.ts b/db/schema/taxtypes.ts new file mode 100644 index 0000000..5d3c45d --- /dev/null +++ b/db/schema/taxtypes.ts @@ -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 diff --git a/db/schema/tenants.ts b/db/schema/tenants.ts new file mode 100644 index 0000000..b798aab --- /dev/null +++ b/db/schema/tenants.ts @@ -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 diff --git a/db/schema/texttemplates.ts b/db/schema/texttemplates.ts new file mode 100644 index 0000000..baddc0d --- /dev/null +++ b/db/schema/texttemplates.ts @@ -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 diff --git a/db/schema/units.ts b/db/schema/units.ts new file mode 100644 index 0000000..61e5e11 --- /dev/null +++ b/db/schema/units.ts @@ -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 diff --git a/db/schema/user_credentials.ts b/db/schema/user_credentials.ts new file mode 100644 index 0000000..f1ae045 --- /dev/null +++ b/db/schema/user_credentials.ts @@ -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 diff --git a/db/schema/vehicles.ts b/db/schema/vehicles.ts new file mode 100644 index 0000000..30a6553 --- /dev/null +++ b/db/schema/vehicles.ts @@ -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 diff --git a/db/schema/vendors.ts b/db/schema/vendors.ts new file mode 100644 index 0000000..32ae94f --- /dev/null +++ b/db/schema/vendors.ts @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c6a115c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + backend: + image: reg.federspiel.software/fedeo/backend:main + restart: always + + environment: + diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..d2b0225 --- /dev/null +++ b/drizzle.config.ts @@ -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, + }, +}) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b2b772 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/scripts/generate-schema-index.ts b/scripts/generate-schema-index.ts new file mode 100644 index 0000000..a7e6c29 --- /dev/null +++ b/scripts/generate-schema-index.ts @@ -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") \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d4155be --- /dev/null +++ b/src/index.ts @@ -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(); \ No newline at end of file diff --git a/src/modules/cron/bankstatementsync.service.ts b/src/modules/cron/bankstatementsync.service.ts new file mode 100644 index 0000000..66863b0 --- /dev/null +++ b/src/modules/cron/bankstatementsync.service.ts @@ -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 => { + 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( + `${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") + } + } +} diff --git a/src/modules/cron/dokuboximport.service.ts b/src/modules/cron/dokuboximport.service.ts new file mode 100644 index 0000000..dd1aaec --- /dev/null +++ b/src/modules/cron/dokuboximport.service.ts @@ -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 + } +} diff --git a/src/modules/cron/prepareIncomingInvoices.ts b/src/modules/cron/prepareIncomingInvoices.ts new file mode 100644 index 0000000..011c1a3 --- /dev/null +++ b/src/modules/cron/prepareIncomingInvoices.ts @@ -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.") + + } + } + +} diff --git a/src/modules/helpdesk/helpdesk.contact.service.ts b/src/modules/helpdesk/helpdesk.contact.service.ts new file mode 100644 index 0000000..f8337df --- /dev/null +++ b/src/modules/helpdesk/helpdesk.contact.service.ts @@ -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 +} diff --git a/src/modules/helpdesk/helpdesk.conversation.service.ts b/src/modules/helpdesk/helpdesk.conversation.service.ts new file mode 100644 index 0000000..b7edc2b --- /dev/null +++ b/src/modules/helpdesk/helpdesk.conversation.service.ts @@ -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 +} diff --git a/src/modules/helpdesk/helpdesk.message.service.ts b/src/modules/helpdesk/helpdesk.message.service.ts new file mode 100644 index 0000000..2d942eb --- /dev/null +++ b/src/modules/helpdesk/helpdesk.message.service.ts @@ -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 +} diff --git a/src/modules/notification.service.ts b/src/modules/notification.service.ts new file mode 100644 index 0000000..241fea4 --- /dev/null +++ b/src/modules/notification.service.ts @@ -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; +} + +export interface UserDirectoryInfo { + email?: string; +} + +export type UserDirectory = (server: FastifyInstance, userId: string, tenantId: number) => Promise; + +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 ` + +
+

FEDEO

+

${this.escapeHtml(title)}

+

${this.nl2br(this.escapeHtml(message))}

+
+

Automatisch generiert von FEDEO

+
+ + `; + } + + // simple escaping (ausreichend für unser Template) + private escapeHtml(s: string) { + return s + .replace(/&/g, '&') + .replace(//g, '>'); + } + + private nl2br(s: string) { + return s.replace(/\n/g, '
'); + } +} diff --git a/src/modules/publiclinks.service.ts b/src/modules/publiclinks.service.ts new file mode 100644 index 0000000..e9b450a --- /dev/null +++ b/src/modules/publiclinks.service.ts @@ -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, + 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> = {}; + + // --------------------------------------------------------- + // 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 = { + 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}; + } +} \ No newline at end of file diff --git a/src/modules/serialexecution.service.ts b/src/modules/serialexecution.service.ts new file mode 100644 index 0000000..e7166b4 --- /dev/null +++ b/src/modules/serialexecution.service.ts @@ -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 = {}; + + // 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 = {}; + 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: [] + }; +} diff --git a/src/modules/time/buildtimeevaluation.service.ts b/src/modules/time/buildtimeevaluation.service.ts new file mode 100644 index 0000000..e420e7c --- /dev/null +++ b/src/modules/time/buildtimeevaluation.service.ts @@ -0,0 +1,229 @@ +// src/services/buildTimeEvaluationFromSpans.ts + +import { FastifyInstance } from "fastify"; +import { and, eq, gte, lte, inArray } from "drizzle-orm"; +import { authProfiles, holidays } from "../../../db/schema"; +import { DerivedSpan } from "./derivetimespans.service"; // Importiert den angereicherten Span-Typ + +// Definiert das erwartete Rückgabeformat +export type TimeEvaluationResult = { + user_id: string; + tenant_id: number; + from: string; + to: string; + + // Sollzeit + timeSpanWorkingMinutes: number; + + // Arbeitszeit Salden + sumWorkingMinutesSubmitted: number; + sumWorkingMinutesApproved: number; + + // Abwesenheiten (minuten und Tage) + sumWorkingMinutesRecreationDays: number; + sumRecreationDays: number; + sumWorkingMinutesVacationDays: number; + sumVacationDays: number; + sumWorkingMinutesSickDays: number; + sumSickDays: number; + + // Endsalden + saldoApproved: number; // Saldo basierend auf genehmigter Zeit + saldoSubmitted: number; // Saldo basierend auf eingereichter/genehmigter Zeit + + spans: DerivedSpan[]; +}; + +// Hilfsfunktion zur Berechnung der Minuten (nur für geschlossene Spannen) +const calcMinutes = (start: Date, end: Date | null): number => { + if (!end) return 0; + return (end.getTime() - start.getTime()) / 60000; +}; + + +export async function buildTimeEvaluationFromSpans( + server: FastifyInstance, + user_id: string, + tenant_id: number, + startDateInput: string, + endDateInput: string, + // Der wichtigste Unterschied: Wir nehmen die angereicherten Spannen als Input + spans: DerivedSpan[] +): Promise { + + const startDate = server.dayjs(startDateInput); + const endDate = server.dayjs(endDateInput); + + // ------------------------------------------------------------- + // 1️⃣ Profil und Feiertage laden (WIE IM ALTEN SERVICE) + // ------------------------------------------------------------- + + const profileRows = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.user_id, user_id), + eq(authProfiles.tenant_id, tenant_id) + ) + ) + .limit(1); + + const profile = profileRows[0]; + if (!profile) throw new Error("Profil konnte nicht geladen werden."); + + const holidaysRows = await server.db + .select({ + date: holidays.date, + }) + .from(holidays) + .where( + and( + inArray(holidays.state_code, [profile.state_code, "DE"]), + gte(holidays.date, startDate.format("YYYY-MM-DD")), + lte(holidays.date, endDate.add(1, "day").format("YYYY-MM-DD")) + ) + ); + + // ------------------------------------------------------------- + // 2️⃣ Sollzeit berechnen (WIE IM ALTEN SERVICE) + // ------------------------------------------------------------- + let timeSpanWorkingMinutes = 0; + const totalDays = endDate.add(1, "day").diff(startDate, "days"); + + for (let i = 0; i < totalDays; i++) { + const date = startDate.add(i, "days"); + const weekday = date.day(); + timeSpanWorkingMinutes += + (profile.weekly_regular_working_hours?.[weekday] || 0) * 60; + } + + + // ------------------------------------------------------------- + // 3️⃣ Arbeits- und Abwesenheitszeiten berechnen (NEUE LOGIK) + // ------------------------------------------------------------- + + let sumWorkingMinutesSubmitted = 0; + let sumWorkingMinutesApproved = 0; + + let sumWorkingMinutesVacationDays = 0; + let sumVacationDays = 0; + let sumWorkingMinutesSickDays = 0; + let sumSickDays = 0; + + // Akkumulieren der Zeiten basierend auf dem abgeleiteten Typ und Status + for (const span of spans) { + + // **A. Arbeitszeiten (WORK)** + if (span.type === "work") { + const minutes = calcMinutes(span.startedAt, span.endedAt); + + // Zähle zur eingereichten Summe, wenn der Status submitted oder approved ist + if (span.status === "submitted" || span.status === "approved") { + sumWorkingMinutesSubmitted += minutes; + } + + // Zähle zur genehmigten Summe, wenn der Status approved ist + if (span.status === "approved") { + sumWorkingMinutesApproved += minutes; + } + } + + // **B. Abwesenheiten (VACATION, SICK)** + // Wir verwenden die Logik aus dem alten Service: Berechnung der Sollzeit + // basierend auf den Tagen der Span (Voraussetzung: Spannen sind Volltages-Spannen) + if (span.type === "vacation" || span.type === "sick") { + + // Behandle nur genehmigte Abwesenheiten für die Saldenberechnung + if (span.status !== "approved") { + continue; + } + + const startDay = server.dayjs(span.startedAt).startOf('day'); + // Wenn endedAt null ist (offene Span), nehmen wir das Ende des Zeitraums + const endDay = span.endedAt ? server.dayjs(span.endedAt).startOf('day') : endDate.startOf('day'); + + // Berechnung der Tage der Span + const days = endDay.diff(startDay, "day") + 1; + + for (let i = 0; i < days; i++) { + const day = startDay.add(i, "day"); + const weekday = day.day(); + const hours = profile.weekly_regular_working_hours?.[weekday] || 0; + + if (span.type === "vacation") { + sumWorkingMinutesVacationDays += hours * 60; + } else if (span.type === "sick") { + sumWorkingMinutesSickDays += hours * 60; + } + } + + if (span.type === "vacation") { + sumVacationDays += days; + } else if (span.type === "sick") { + sumSickDays += days; + } + } + + // PAUSE Spannen werden ignoriert, da sie in der faktischen Ableitung bereits von WORK abgezogen wurden. + } + + + // ------------------------------------------------------------- + // 4️⃣ Feiertagsausgleich (WIE IM ALTEN SERVICE) + // ------------------------------------------------------------- + let sumWorkingMinutesRecreationDays = 0; + let sumRecreationDays = 0; + + if (profile.recreation_days_compensation && holidaysRows?.length) { + holidaysRows.forEach(({ date }) => { + const weekday = server.dayjs(date).day(); + const hours = profile.weekly_regular_working_hours?.[weekday] || 0; + sumWorkingMinutesRecreationDays += hours * 60; + sumRecreationDays++; + }); + } + + // ------------------------------------------------------------- + // 5️⃣ Salden berechnen (NEUE LOGIK) + // ------------------------------------------------------------- + + const totalCompensatedMinutes = + sumWorkingMinutesRecreationDays + + sumWorkingMinutesVacationDays + + sumWorkingMinutesSickDays; + + // Saldo basierend auf GENEHMIGTER Arbeitszeit + const totalApprovedMinutes = sumWorkingMinutesApproved + totalCompensatedMinutes; + const saldoApproved = totalApprovedMinutes - timeSpanWorkingMinutes; + + // Saldo basierend auf EINGEREICHTER und GENEHMIGTER Arbeitszeit + const totalSubmittedMinutes = sumWorkingMinutesSubmitted + totalCompensatedMinutes; + const saldoSubmitted = totalSubmittedMinutes - timeSpanWorkingMinutes; + + + // ------------------------------------------------------------- + // 6️⃣ Rückgabe + // ------------------------------------------------------------- + return { + user_id, + tenant_id, + from: startDate.format("YYYY-MM-DD"), + to: endDate.format("YYYY-MM-DD"), + timeSpanWorkingMinutes, + + sumWorkingMinutesSubmitted, + sumWorkingMinutesApproved, + + sumWorkingMinutesRecreationDays, + sumRecreationDays, + sumWorkingMinutesVacationDays, + sumVacationDays, + sumWorkingMinutesSickDays, + sumSickDays, + + saldoApproved, + saldoSubmitted, + spans, + }; +} \ No newline at end of file diff --git a/src/modules/time/derivetimespans.service.ts b/src/modules/time/derivetimespans.service.ts new file mode 100644 index 0000000..2c18f25 --- /dev/null +++ b/src/modules/time/derivetimespans.service.ts @@ -0,0 +1,165 @@ +type State = "IDLE" | "WORKING" | "PAUSED" | "ABSENT"; + +export type SpanStatus = "factual" | "submitted" | "approved" | "rejected"; + +export type DerivedSpan = { + type: "work" | "pause" | "vacation" | "sick" | "overtime_compensation"; + startedAt: Date; + endedAt: Date | null; + sourceEventIds: string[]; + status: SpanStatus; + statusActorId?: string; +}; + +type TimeEvent = { + id: string; + eventtype: string; + eventtime: Date; +}; + +// Liste aller faktischen Event-Typen, die die Zustandsmaschine beeinflussen +const FACTUAL_EVENT_TYPES = new Set([ + "work_start", + "pause_start", + "pause_end", + "work_end", + "auto_stop", // Wird als work_end behandelt + "vacation_start", + "vacation_end", + "sick_start", + "sick_end", + "overtime_compensation_start", + "overtime_compensation_end", +]); + +export function deriveTimeSpans(allValidEvents: TimeEvent[]): DerivedSpan[] { + + // 1. FILTERN: Nur faktische Events verarbeiten + const events = allValidEvents.filter(event => + FACTUAL_EVENT_TYPES.has(event.eventtype) + ); + + const spans: DerivedSpan[] = []; + + let state: State = "IDLE"; + let currentStart: Date | null = null; + let currentType: DerivedSpan["type"] | null = null; + let sourceEventIds: string[] = []; + + const closeSpan = (end: Date) => { + if (!currentStart || !currentType) return; + + spans.push({ + type: currentType, + startedAt: currentStart, + endedAt: end, + sourceEventIds: [...sourceEventIds], + // Standardstatus ist "factual", wird später angereichert + status: "factual" + }); + + currentStart = null; + currentType = null; + sourceEventIds = []; + }; + + const closeOpenSpanAsRunning = () => { + if (!currentStart || !currentType) return; + + spans.push({ + type: currentType, + startedAt: currentStart, + endedAt: null, + sourceEventIds: [...sourceEventIds], + // Standardstatus ist "factual", wird später angereichert + status: "factual" + }); + + currentStart = null; + currentType = null; + sourceEventIds = []; + }; + + for (const event of events) { + sourceEventIds.push(event.id); + + switch (event.eventtype) { + /* ========================= + ARBEITSZEIT + ========================= */ + + case "work_start": + if (state === "WORKING" || state === "PAUSED" || state === "ABSENT") { + // Schließt die vorherige Spanne (falls z.B. work_start nach sick_start kommt) + closeSpan(event.eventtime); + } + state = "WORKING"; + currentStart = event.eventtime; + currentType = "work"; + break; + + case "pause_start": + if (state === "WORKING") { + closeSpan(event.eventtime); + state = "PAUSED"; + currentStart = event.eventtime; + currentType = "pause"; + } + break; + + case "pause_end": + if (state === "PAUSED") { + closeSpan(event.eventtime); + state = "WORKING"; + currentStart = event.eventtime; + currentType = "work"; + } + break; + + case "work_end": + case "auto_stop": + if (state === "WORKING" || state === "PAUSED") { + closeSpan(event.eventtime); + } + state = "IDLE"; + break; + + /* ========================= + ABWESENHEITEN + ========================= */ + + case "vacation_start": + case "sick_start": + case "overtime_compensation_start": + // Mappt den Event-Typ direkt auf den Span-Typ + const newType = event.eventtype.split('_')[0] as DerivedSpan["type"]; + + if (state !== "IDLE") { + closeSpan(event.eventtime); + } + state = "ABSENT"; + currentStart = event.eventtime; + currentType = newType; + break; + + case "vacation_end": + case "sick_end": + case "overtime_compensation_end": + // Extrahiert den Typ der zu beendenden Spanne + const endedType = event.eventtype.split('_')[0] as DerivedSpan["type"]; + + if (state === "ABSENT" && currentType === endedType) { + closeSpan(event.eventtime); + } + state = "IDLE"; + break; + } + } + + // 🔴 WICHTIG: Offene Spannen als laufend zurückgeben + if (state !== "IDLE") { + closeOpenSpanAsRunning(); + } + + return spans; +} \ No newline at end of file diff --git a/src/modules/time/enrichtimespanswithstatus.service.ts b/src/modules/time/enrichtimespanswithstatus.service.ts new file mode 100644 index 0000000..b82f2eb --- /dev/null +++ b/src/modules/time/enrichtimespanswithstatus.service.ts @@ -0,0 +1,91 @@ +// src/services/enrichSpansWithStatus.ts (Korrigierte Version) + +import { DerivedSpan, SpanStatus } from "./derivetimespans.service"; +import { TimeEvent } from "./loadvalidevents.service"; // Jetzt mit related_event_id und actoruser_id + +// ... (Rest der Imports) + +export function enrichSpansWithStatus( + factualSpans: DerivedSpan[], + allValidEvents: TimeEvent[] +): DerivedSpan[] { + + // 1. Map der administrativen Aktionen erstellen + const eventStatusMap = new Map(); + + const administrativeEvents = allValidEvents.filter(e => + e.eventtype === 'submitted' || e.eventtype === 'approved' || e.eventtype === 'rejected' + ); + + // allValidEvents ist nach Zeit sortiert + for (const event of administrativeEvents) { + + // **Verwendung des expliziten Feldes** + const relatedId = event.related_event_id; + const actorId = event.actoruser_id; + + if (relatedId) { // Nur fortfahren, wenn ein Bezugs-Event existiert + + let status: SpanStatus = "factual"; + // Wir überschreiben den Status des relatedId basierend auf der Event-Historie + if (event.eventtype === 'submitted') status = 'submitted'; + else if (event.eventtype === 'approved') status = 'approved'; + else if (event.eventtype === 'rejected') status = 'rejected'; + + eventStatusMap.set(relatedId, { status, actorId }); + } + } + + // 2. Status der Spannen bestimmen und anreichern + return factualSpans.map(span => { + + let approvedCount = 0; + let rejectedCount = 0; + let submittedCount = 0; + let isFactualCount = 0; + + for (const sourceId of span.sourceEventIds) { + const statusInfo = eventStatusMap.get(sourceId); + + if (statusInfo) { + // Ein faktisches Event kann durch mehrere administrative Events betroffen sein + // Wir speichern im Map nur den letzten Status (z.B. approved überschreibt submitted) + if (statusInfo.status === 'approved') approvedCount++; + else if (statusInfo.status === 'rejected') rejectedCount++; + else if (statusInfo.status === 'submitted') submittedCount++; + } else { + // Wenn kein administratives Event existiert + isFactualCount++; + } + } + + // Regel zur Bestimmung des Span-Status: + const totalSourceEvents = span.sourceEventIds.length; + let finalStatus: SpanStatus = "factual"; + + if (totalSourceEvents > 0) { + + // Priorität 1: Rejection + if (rejectedCount > 0) { + finalStatus = "rejected"; + } + // Priorität 2: Full Approval + else if (approvedCount === totalSourceEvents) { + finalStatus = "approved"; + } + // Priorität 3: Submitted (wenn nicht fully approved oder rejected, aber mindestens eines submitted ist) + else if (submittedCount > 0 || approvedCount > 0) { + finalStatus = "submitted"; + // Ein Span ist submitted, wenn es zumindest teilweise eingereicht (oder genehmigt) ist, + // aber nicht alle Events den finalen Status "approved" haben. + } + // Ansonsten bleibt es "factual" (wenn z.B. nur work_start aber nicht work_end eingereicht wurde, oder nichts) + } + + // Rückgabe der angereicherten Span + return { + ...span, + status: finalStatus, + }; + }); +} \ No newline at end of file diff --git a/src/modules/time/evaluation.service.ts b/src/modules/time/evaluation.service.ts new file mode 100644 index 0000000..8032d32 --- /dev/null +++ b/src/modules/time/evaluation.service.ts @@ -0,0 +1,232 @@ +import { FastifyInstance } from "fastify"; +import {and, eq, gte, lte, asc, inArray} from "drizzle-orm"; +import { + authProfiles, + stafftimeentries, + holidays, +} from "../../../db/schema"; + +export async function generateTimesEvaluation( + server: FastifyInstance, + user_id: string, + tenant_id: number, + startDateInput: string, + endDateInput: string +) { + const startDate = server.dayjs(startDateInput); + const endDate = server.dayjs(endDateInput); + + console.log(startDate.format("YYYY-MM-DD HH:mm:ss")); + console.log(endDate.format("YYYY-MM-DD HH:mm:ss")); + + // ------------------------------------------------------------- + // 1️⃣ Profil laden + // ------------------------------------------------------------- + const profileRows = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.user_id, user_id), + eq(authProfiles.tenant_id, tenant_id) + ) + ) + .limit(1); + + const profile = profileRows[0]; + + if (!profile) throw new Error("Profil konnte nicht geladen werden."); + + // ------------------------------------------------------------- + // 2️⃣ Arbeitszeiten laden + // ------------------------------------------------------------- + const timesRaw = await server.db + .select() + .from(stafftimeentries) + .where( + and( + eq(stafftimeentries.tenant_id, tenant_id), + eq(stafftimeentries.user_id, user_id) + ) + ) + .orderBy(asc(stafftimeentries.started_at)); + + const isBetween = (spanStartDate, spanEndDate, startDate, endDate) => { + return ( + server + .dayjs(startDate) + .isBetween(spanStartDate, spanEndDate, "day", "[]") && + server + .dayjs(endDate) + .isBetween(spanStartDate, spanEndDate, "day", "[]") + ); + }; + + const times = timesRaw.filter((i) => + isBetween(startDate, endDate, i.started_at, i.stopped_at) + ); + + console.log(times); + + // ------------------------------------------------------------- + // 3️⃣ Feiertage laden + // ------------------------------------------------------------- + const holidaysRows = await server.db + .select({ + date: holidays.date, + }) + .from(holidays) + .where( + and( + inArray(holidays.state_code, [profile.state_code, "DE"]), + gte(holidays.date, startDate.format("YYYY-MM-DD")), + lte(holidays.date, endDate.add(1, "day").format("YYYY-MM-DD")) + ) + ); + + // ------------------------------------------------------------- + // 4️⃣ Sollzeit berechnen + // ------------------------------------------------------------- + let timeSpanWorkingMinutes = 0; + const totalDays = endDate.add(1, "day").diff(startDate, "days"); + + for (let i = 0; i < totalDays; i++) { + const date = startDate.add(i, "days"); + const weekday = date.day(); + timeSpanWorkingMinutes += + (profile.weekly_regular_working_hours?.[weekday] || 0) * 60; + } + + // ------------------------------------------------------------- + // 5️⃣ Eingereicht/genehmigt + // ------------------------------------------------------------- + const calcMinutes = (start: string, end: string | null) => + server.dayjs(end || new Date()).diff(server.dayjs(start), "minutes"); + + let sumWorkingMinutesEingereicht = 0; + let sumWorkingMinutesApproved = 0; + + for (const t of times) { + // @ts-ignore + const minutes = calcMinutes(t.started_at, t.stopped_at); + + if (["submitted", "approved"].includes(t.state) && t.type === "work") { + sumWorkingMinutesEingereicht += minutes; + } + if (t.state === "approved" && t.type === "work") { + sumWorkingMinutesApproved += minutes; + } + } + + // ------------------------------------------------------------- + // 6️⃣ Feiertagsausgleich + // ------------------------------------------------------------- + let sumWorkingMinutesRecreationDays = 0; + let sumRecreationDays = 0; + + if (profile.recreation_days_compensation && holidaysRows?.length) { + holidaysRows.forEach(({ date }) => { + const weekday = server.dayjs(date).day(); + const hours = profile.weekly_regular_working_hours?.[weekday] || 0; + sumWorkingMinutesRecreationDays += hours * 60; + sumRecreationDays++; + }); + } + + // ------------------------------------------------------------- + // 7️⃣ Urlaub + // ------------------------------------------------------------- + let sumWorkingMinutesVacationDays = 0; + let sumVacationDays = 0; + + times + .filter((t) => t.type === "vacation" && t.state === "approved") + .forEach((time) => { + // Tippfehler aus Original: startet_at vs started_at → NICHT korrigiert + const days = + server.dayjs(time.stopped_at).diff( + //@ts-ignore + server.dayjs(time.startet_at), + "day" + ) + 1; + + for (let i = 0; i < days; i++) { + const weekday = server + .dayjs(time.started_at) + .add(i, "day") + .day(); + const hours = + profile.weekly_regular_working_hours?.[weekday] || 0; + sumWorkingMinutesVacationDays += hours * 60; + } + sumVacationDays += days; + }); + + // ------------------------------------------------------------- + // 8️⃣ Krankheit + // ------------------------------------------------------------- + let sumWorkingMinutesSickDays = 0; + let sumSickDays = 0; + + times + .filter((t) => t.type === "sick" && t.state === "approved") + .forEach((time) => { + const days = + server.dayjs(time.stopped_at).diff( + //@ts-ignore + server.dayjs(time.startet_at), + "day" + ) + 1; + + for (let i = 0; i < days; i++) { + const weekday = server + .dayjs(time.started_at) + .add(i, "day") + .day(); + const hours = + profile.weekly_regular_working_hours?.[weekday] || 0; + sumWorkingMinutesSickDays += hours * 60; + } + + sumSickDays += days; + }); + + // ------------------------------------------------------------- + // 9️⃣ Salden + // ------------------------------------------------------------- + const saldo = + sumWorkingMinutesApproved + + sumWorkingMinutesRecreationDays + + sumWorkingMinutesVacationDays + + sumWorkingMinutesSickDays - + timeSpanWorkingMinutes; + + const saldoInOfficial = + sumWorkingMinutesEingereicht + + sumWorkingMinutesRecreationDays + + sumWorkingMinutesVacationDays + + sumWorkingMinutesSickDays - + timeSpanWorkingMinutes; + + // ------------------------------------------------------------- + // 🔟 Rückgabe identisch + // ------------------------------------------------------------- + return { + user_id, + tenant_id, + from: startDate.format("YYYY-MM-DD"), + to: endDate.format("YYYY-MM-DD"), + timeSpanWorkingMinutes, + sumWorkingMinutesEingereicht, + sumWorkingMinutesApproved, + sumWorkingMinutesRecreationDays, + sumRecreationDays, + sumWorkingMinutesVacationDays, + sumVacationDays, + sumWorkingMinutesSickDays, + sumSickDays, + saldo, + saldoInOfficial, + times, + }; +} diff --git a/src/modules/time/loadvalidevents.service.ts b/src/modules/time/loadvalidevents.service.ts new file mode 100644 index 0000000..134e363 --- /dev/null +++ b/src/modules/time/loadvalidevents.service.ts @@ -0,0 +1,105 @@ +// src/services/loadValidEvents.ts + +import { stafftimeevents } from "../../../db/schema"; +import {sql, and, eq, gte, lte, inArray} from "drizzle-orm"; +import { FastifyInstance } from "fastify"; + +export type TimeType = "work_start" | "work_end" | "pause_start" | "pause_end" | "sick_start" | "sick_end" | "vacation_start" | "vacation_end" | "ovetime_compensation_start" | "overtime_compensation_end"; + + +// Die Definition des TimeEvent Typs, der zurückgegeben wird (muss mit dem tatsächlichen Typ übereinstimmen) +export type TimeEvent = { + id: string; + eventtype: string; + eventtime: Date; + actoruser_id: string; + related_event_id: string | null; + // Fügen Sie hier alle weiteren Felder hinzu, die Sie benötigen +}; + +export async function loadValidEvents( + server: FastifyInstance, + tenantId: number, + userId: string, + from: Date, + to: Date +): Promise { + // Definieren Sie einen Alias für die stafftimeevents Tabelle in der äußeren Abfrage + const baseEvents = stafftimeevents; + + // Die Subquery, um alle IDs zu finden, die ungültig gemacht wurden + // Wir nennen die innere Tabelle 'invalidatingEvents' + const invalidatingEvents = server.db + .select({ + invalidatedId: baseEvents.invalidates_event_id + }) + .from(baseEvents) + .as('invalidating_events'); + + // Die Hauptabfrage + const result = await server.db + .select() + .from(baseEvents) + .where( + and( + // 1. Tenant und User filtern + eq(baseEvents.tenant_id, tenantId), + eq(baseEvents.user_id, userId), + + // 2. Zeitbereich filtern (Typensicher) + gte(baseEvents.eventtime, from), + lte(baseEvents.eventtime, to), + + // 3. WICHTIG: Korrekturen ausschließen (NOT EXISTS) + // Schließe jedes Event aus, dessen ID in der Liste der invalidates_event_id erscheint. + sql` + not exists ( + select 1 + from ${stafftimeevents} i + where i.invalidates_event_id = ${baseEvents.id} + ) + ` + ) + ) + .orderBy( + // Wichtig für die deriveTimeSpans Logik: Event-Zeit muss primär sein + baseEvents.eventtime, + baseEvents.created_at, // Wenn Eventtime identisch ist, das später erstellte zuerst + baseEvents.id + ); + + // Mapping auf den sauberen TimeEvent Typ + return result.map(e => ({ + id: e.id, + eventtype: e.eventtype, + eventtime: e.eventtime, + // Fügen Sie hier alle weiteren benötigten Felder hinzu (z.B. metadata, actoruser_id) + // ... + })) as TimeEvent[]; +} + +export async function loadRelatedAdminEvents(server, eventIds) { + if (eventIds.length === 0) return []; + + // Lädt alle administrativen Events, die sich auf die faktischen Event-IDs beziehen + const adminEvents = await server.db + .select() + .from(stafftimeevents) + .where( + and( + inArray(stafftimeevents.related_event_id, eventIds), + // Wir müssen hier die Entkräftung prüfen, um z.B. einen abgelehnten submitted-Event auszuschließen + sql` + not exists ( + select 1 + from ${stafftimeevents} i + where i.invalidates_event_id = ${stafftimeevents}.id + ) + `, + ) + ) + // Muss nach Zeit sortiert sein, um den Status korrekt zu bestimmen! + .orderBy(stafftimeevents.eventtime); + + return adminEvents; +} \ No newline at end of file diff --git a/src/plugins/auth.m2m.ts b/src/plugins/auth.m2m.ts new file mode 100644 index 0000000..58e6c3c --- /dev/null +++ b/src/plugins/auth.m2m.ts @@ -0,0 +1,51 @@ +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import { secrets } from "../utils/secrets"; + +/** + * Fastify Plugin für Machine-to-Machine Authentifizierung. + * + * Dieses Plugin prüft, ob der Header `x-api-key` vorhanden ist + * und mit dem in der .env hinterlegten M2M_API_KEY übereinstimmt. + * + * Verwendung: + * server.register(m2mAuthPlugin, { allowedPrefix: '/internal' }) + */ +export default fp(async (server: FastifyInstance, opts: { allowedPrefix?: string } = {}) => { + //const allowedPrefix = opts.allowedPrefix || "/internal"; + + server.addHook("preHandler", async (req, reply) => { + try { + // Nur prüfen, wenn Route unterhalb des Prefix liegt + //if (!req.url.startsWith(allowedPrefix)) return; + + const apiKey = req.headers["x-api-key"]; + + if (!apiKey || apiKey !== secrets.M2M_API_KEY) { + server.log.warn(`[M2M Auth] Ungültiger oder fehlender API-Key bei ${req.url}`); + return reply.status(401).send({ error: "Unauthorized" }); + } + + // Zusatzinformationen im Request (z. B. interne Kennung) + (req as any).m2m = { + verified: true, + type: "internal", + key: apiKey, + }; + } catch (err) { + // @ts-ignore + server.log.error("[M2M Auth] Fehler beim Prüfen des API-Keys:", err); + return reply.status(500).send({ error: "Internal Server Error" }); + } + }); +}); + +declare module "fastify" { + interface FastifyRequest { + m2m?: { + verified: boolean; + type: "internal"; + key: string; + }; + } +} diff --git a/src/plugins/auth.ts b/src/plugins/auth.ts new file mode 100644 index 0000000..f4b1427 --- /dev/null +++ b/src/plugins/auth.ts @@ -0,0 +1,115 @@ +import { FastifyInstance } from "fastify" +import fp from "fastify-plugin" +import jwt from "jsonwebtoken" +import { secrets } from "../utils/secrets" + +import { + authUserRoles, + authRolePermissions, +} from "../../db/schema" + +import { eq, and } from "drizzle-orm" + +export default fp(async (server: FastifyInstance) => { + server.addHook("preHandler", async (req, reply) => { + // 1️⃣ Token aus Header oder Cookie lesen + const cookieToken = req.cookies?.token + const authHeader = req.headers.authorization + + const headerToken = + authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null + + const token = + headerToken && headerToken.length > 10 + ? headerToken + : cookieToken || null + + if (!token) { + return reply.code(401).send({ error: "Authentication required" }) + } + + try { + // 2️⃣ JWT verifizieren + const payload = jwt.verify(token, secrets.JWT_SECRET!) as { + user_id: string + email: string + tenant_id: number | null + } + + if (!payload?.user_id) { + return reply.code(401).send({ error: "Invalid token" }) + } + + // Payload an Request hängen + req.user = payload + + // Multi-Tenant Modus ohne ausgewählten Tenant → keine Rollenprüfung + if (!req.user.tenant_id) { + return + } + + const tenantId = req.user.tenant_id + const userId = req.user.user_id + + // -------------------------------------------------------- + // 3️⃣ Rolle des Nutzers im Tenant holen + // -------------------------------------------------------- + const roleRows = await server.db + .select() + .from(authUserRoles) + .where( + and( + eq(authUserRoles.user_id, userId), + eq(authUserRoles.tenant_id, tenantId) + ) + ) + .limit(1) + + if (roleRows.length === 0) { + return reply + .code(403) + .send({ error: "No role assigned for this tenant" }) + } + + const roleId = roleRows[0].role_id + + // -------------------------------------------------------- + // 4️⃣ Berechtigungen der Rolle laden + // -------------------------------------------------------- + const permissionRows = await server.db + .select() + .from(authRolePermissions) + .where(eq(authRolePermissions.role_id, roleId)) + + const permissions = permissionRows.map((p) => p.permission) + + // -------------------------------------------------------- + // 5️⃣ An Request hängen für spätere Nutzung + // -------------------------------------------------------- + req.role = roleId + req.permissions = permissions + req.hasPermission = (perm: string) => permissions.includes(perm) + + } catch (err) { + console.error("JWT verification error:", err) + return reply.code(401).send({ error: "Invalid or expired token" }) + } + }) +}) + +// --------------------------------------------------------------------------- +// Fastify TypeScript Erweiterungen +// --------------------------------------------------------------------------- + +declare module "fastify" { + interface FastifyRequest { + user: { + user_id: string + email: string + tenant_id: number | null + } + role: string + permissions: string[] + hasPermission: (permission: string) => boolean + } +} diff --git a/src/plugins/cors.ts b/src/plugins/cors.ts new file mode 100644 index 0000000..e62e9c3 --- /dev/null +++ b/src/plugins/cors.ts @@ -0,0 +1,22 @@ +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import cors from "@fastify/cors"; + +export default fp(async (server: FastifyInstance) => { + await server.register(cors, { + origin: [ + "http://localhost:3000", // dein Nuxt-Frontend + "http://localhost:3001", // dein Nuxt-Frontend + "http://127.0.0.1:3000", // dein Nuxt-Frontend + "http://192.168.1.227:3001", // dein Nuxt-Frontend + "http://192.168.1.227:3000", // dein Nuxt-Frontend + "https://beta.fedeo.de", // dein Nuxt-Frontend + "https://app.fedeo.de", // dein Nuxt-Frontend + "capacitor://localhost", // dein Nuxt-Frontend + ], + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization", "Context", "X-Public-Pin"], + exposedHeaders: ["Authorization", "Content-Disposition", "Content-Type", "Content-Length"], // optional, falls du ihn auch auslesen willst + credentials: true, // wichtig, falls du Cookies nutzt + }); +}); \ No newline at end of file diff --git a/src/plugins/dayjs.ts b/src/plugins/dayjs.ts new file mode 100644 index 0000000..4187d9e --- /dev/null +++ b/src/plugins/dayjs.ts @@ -0,0 +1,41 @@ +import fp from "fastify-plugin" +import dayjs from "dayjs" + +// 🧩 Plugins +import customParseFormat from "dayjs/plugin/customParseFormat.js"; +import isBetween from "dayjs/plugin/isBetween.js"; +import duration from "dayjs/plugin/duration.js"; +import utc from "dayjs/plugin/utc" +import timezone from "dayjs/plugin/timezone" +import isSameOrAfter from "dayjs/plugin/isSameOrAfter" +import isSameOrBefore from "dayjs/plugin/isSameOrBefore" +import isoWeek from "dayjs/plugin/isoWeek" +import localizedFormat from "dayjs/plugin/localizedFormat" + +// 🔧 Erweiterungen aktivieren +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) +dayjs.extend(isBetween) +dayjs.extend(isoWeek) +dayjs.extend(localizedFormat) +dayjs.extend(customParseFormat) +dayjs.extend(isBetween) +dayjs.extend(duration) + +/** + * Fastify Plugin: hängt dayjs an den Server an + */ +export default fp(async (server) => { + server.decorate("dayjs", dayjs) +}) + +/** + * Typ-Erweiterung für TypeScript + */ +declare module "fastify" { + interface FastifyInstance { + dayjs: typeof dayjs + } +} diff --git a/src/plugins/db.ts b/src/plugins/db.ts new file mode 100644 index 0000000..8c1f61a --- /dev/null +++ b/src/plugins/db.ts @@ -0,0 +1,34 @@ +import fp from "fastify-plugin" +import {drizzle, NodePgDatabase} from "drizzle-orm/node-postgres" +import { Pool } from "pg" +import * as schema from "../../db/schema" + +export default fp(async (server, opts) => { + const pool = new Pool({ + host: "100.102.185.225", + port: Number(process.env.DB_PORT || 5432), + user: "postgres", + password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu", + database: "fedeo", + ssl: process.env.DB_DISABLE_SSL === "true" ? false : undefined, + }) + + // Drizzle instance + const db = drizzle(pool, { schema }) + + // Dekorieren -> überall server.db + server.decorate("db", db) + + // Graceful Shutdown + server.addHook("onClose", async () => { + await pool.end() + }) + + server.log.info("Drizzle database connected") +}) + +declare module "fastify" { + interface FastifyInstance { + db:NodePgDatabase + } +} diff --git a/src/plugins/queryconfig.ts b/src/plugins/queryconfig.ts new file mode 100644 index 0000000..3ecb8c0 --- /dev/null +++ b/src/plugins/queryconfig.ts @@ -0,0 +1,125 @@ +import fp from 'fastify-plugin' +import { FastifyPluginAsync, FastifyRequest } from 'fastify' + +export interface QueryConfigPagination { + page: number + limit: number + offset: number +} + +export interface QueryConfigSort { + field: string + direction: 'asc' | 'desc' +} + +export interface QueryConfig { + pagination: QueryConfigPagination | null + sort: QueryConfigSort[] + filters: Record + paginationDisabled: boolean +} + +declare module 'fastify' { + interface FastifyRequest { + queryConfig: QueryConfig + } +} + +interface QueryConfigPluginOptions { + routes?: string[] +} + +function matchRoutePattern(currentPath: string, patterns: string[]): boolean { + return patterns.some(pattern => { + // Beispiel: /users/:id -> /^\/users\/[^/]+$/ + const regex = new RegExp( + '^' + + pattern + .replace(/\*/g, '.*') // wildcard + .replace(/:[^/]+/g, '[^/]+') + + '$' + ) + return regex.test(currentPath) + }) +} + +const queryConfigPlugin: FastifyPluginAsync = async ( + fastify, + opts +) => { + const routePatterns = opts.routes || [] + + fastify.addHook('preHandler', async (req: FastifyRequest, reply) => { + const path = req.routeOptions.url || req.raw.url || '' + + if (!matchRoutePattern(path, routePatterns)) { + return + } + + const query = req.query as Record + + console.log(query) + + // Pagination deaktivieren? + const disablePagination = + query.noPagination === 'true' || + query.pagination === 'false' || + query.limit === '0' + + // Pagination berechnen + let pagination: QueryConfigPagination | null = null + if (!disablePagination) { + const page = Math.max(parseInt(query.page) || 1, 1) + const limit = Math.max(parseInt(query.limit) || 25, 1) + const offset = (page - 1) * limit + pagination = { page, limit, offset } + } + + // Sortierung + const sort: QueryConfigSort[] = [] + if (typeof query.sort === 'string') { + const items = query.sort.split(',') + for (const item of items) { + const [field, direction] = item.split(':') + sort.push({ + field: field.trim(), + direction: (direction || 'asc').toLowerCase() === 'desc' ? 'desc' : 'asc' + }) + } + } + + // Filterung + const filters: Record = {} + + for (const [key, value] of Object.entries(query)) { + const match = key.match(/^filter\[(.+)\]$/) + if (!match) continue + + const filterKey = match[1] + + if (typeof value === 'string') { + // Split bei Komma → mehrere Werte + const parts = value.split(',').map(v => v.trim()).filter(Boolean) + + // Automatische Typkonvertierung je Element + const parsedValues = parts.map(v => { + if (v === 'true') return true + if (v === 'false') return false + if (v === 'null') return null + return v + }) + + filters[filterKey] = parsedValues.length > 1 ? parsedValues : parsedValues[0] + } + } + + req.queryConfig = { + pagination, + sort, + filters, + paginationDisabled: disablePagination + } + }) +} + +export default fp(queryConfigPlugin, { name: 'query-config' }) diff --git a/src/plugins/services.ts b/src/plugins/services.ts new file mode 100644 index 0000000..c5788b7 --- /dev/null +++ b/src/plugins/services.ts @@ -0,0 +1,24 @@ +// /plugins/services.ts +import fp from "fastify-plugin"; +import { bankStatementService } from "../modules/cron/bankstatementsync.service"; +//import {initDokuboxClient, syncDokubox} from "../modules/cron/dokuboximport.service"; +import { FastifyInstance } from "fastify"; +import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices"; + +declare module "fastify" { + interface FastifyInstance { + services: { + bankStatements: ReturnType; + //dokuboxSync: ReturnType; + prepareIncomingInvoices: ReturnType; + }; + } +} + +export default fp(async function servicePlugin(server: FastifyInstance) { + server.decorate("services", { + bankStatements: bankStatementService(server), + //dokuboxSync: syncDokubox(server), + prepareIncomingInvoices: prepareIncomingInvoices(server), + }); +}); diff --git a/src/plugins/supabase.ts b/src/plugins/supabase.ts new file mode 100644 index 0000000..eb5ca98 --- /dev/null +++ b/src/plugins/supabase.ts @@ -0,0 +1,19 @@ +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import {secrets} from "../utils/secrets"; + +export default fp(async (server: FastifyInstance) => { + const supabaseUrl = secrets.SUPABASE_URL + const supabaseServiceKey = secrets.SUPABASE_SERVICE_ROLE_KEY + const supabase: SupabaseClient = createClient(supabaseUrl, supabaseServiceKey); + + // Fastify um supabase erweitern + server.decorate("supabase", supabase); +}); + +declare module "fastify" { + interface FastifyInstance { + supabase: SupabaseClient; + } +} \ No newline at end of file diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts new file mode 100644 index 0000000..b529ea8 --- /dev/null +++ b/src/plugins/swagger.ts @@ -0,0 +1,30 @@ +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import swagger from "@fastify/swagger"; +import swaggerUi from "@fastify/swagger-ui"; + +export default fp(async (server: FastifyInstance) => { + await server.register(swagger, { + mode: "dynamic", // wichtig: generiert echtes OpenAPI JSON + openapi: { + info: { + title: "Multi-Tenant API", + description: "API Dokumentation für dein Backend", + version: "1.0.0", + }, + servers: [{ url: "http://localhost:3000" }], + }, + }); + + // @ts-ignore + await server.register(swaggerUi, { + routePrefix: "/docs", // UI erreichbar unter http://localhost:3000/docs + swagger: { + info: { + title: "Multi-Tenant API", + version: "1.0.0", + }, + }, + exposeRoute: true, + }); +}); \ No newline at end of file diff --git a/src/plugins/tenant.ts b/src/plugins/tenant.ts new file mode 100644 index 0000000..0556b36 --- /dev/null +++ b/src/plugins/tenant.ts @@ -0,0 +1,41 @@ +import { FastifyInstance, FastifyRequest } from "fastify"; +import fp from "fastify-plugin"; + +export default fp(async (server: FastifyInstance) => { + server.addHook("preHandler", async (req, reply) => { + const host = req.headers.host?.split(":")[0]; // Domain ohne Port + if (!host) { + reply.code(400).send({ error: "Missing host header" }); + return; + } + // Tenant aus DB laden + const { data: tenant } = await server.supabase + .from("tenants") + .select("*") + .eq("portalDomain", host) + .single(); + + + if(!tenant) { + // Multi Tenant Mode + (req as any).tenant = null; + }else { + // Tenant ins Request-Objekt hängen + (req as any).tenant = tenant; + } + + }); +}); + +// Typ-Erweiterung +declare module "fastify" { + interface FastifyRequest { + tenant?: { + id: string; + name: string; + domain?: string; + subdomain?: string; + settings?: Record; + }; + } +} \ No newline at end of file diff --git a/src/routes/admin.ts b/src/routes/admin.ts new file mode 100644 index 0000000..9120d18 --- /dev/null +++ b/src/routes/admin.ts @@ -0,0 +1,117 @@ +import { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; + +import { + authTenantUsers, + authUsers, + tenants, +} from "../../db/schema"; + +export default async function adminRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // POST /admin/add-user-to-tenant + // ------------------------------------------------------------- + server.post("/admin/add-user-to-tenant", async (req, reply) => { + try { + const body = req.body as { + user_id: string; + tenant_id: number; + role?: string; + mode?: "single" | "multi"; + }; + + if (!body.user_id || !body.tenant_id) { + return reply.code(400).send({ + error: "user_id and tenant_id required" + }); + } + + const mode = body.mode ?? "multi"; + + // ---------------------------- + // SINGLE MODE → alte Verknüpfungen löschen + // ---------------------------- + if (mode === "single") { + await server.db + .delete(authTenantUsers) + .where(eq(authTenantUsers.user_id, body.user_id)); + } + + // ---------------------------- + // Neue Verknüpfung hinzufügen + // ---------------------------- + + await server.db + .insert(authTenantUsers) + // @ts-ignore + .values({ + user_id: body.user_id, + tenantId: body.tenant_id, + role: body.role ?? "member", + }); + + return { success: true, mode }; + + } catch (err) { + console.error("ERROR /admin/add-user-to-tenant:", err); + return reply.code(500).send({ error: "Internal Server Error" }); + } + }); + + + // ------------------------------------------------------------- + // GET /admin/user-tenants/:user_id + // ------------------------------------------------------------- + server.get("/admin/user-tenants/:user_id", async (req, reply) => { + try { + const { user_id } = req.params as { user_id: string }; + + if (!user_id) { + return reply.code(400).send({ error: "user_id required" }); + } + + // ---------------------------- + // 1) User existiert? + // ---------------------------- + const [user] = await server.db + .select() + .from(authUsers) + .where(eq(authUsers.id, user_id)) + .limit(1); + + if (!user) { + return reply.code(400).send({ error: "faulty user_id presented" }); + } + + // ---------------------------- + // 2) Tenants Join über auth_tenant_users + // ---------------------------- + const tenantRecords = await server.db + .select({ + id: tenants.id, + name: tenants.name, + short: tenants.short, + locked: tenants.locked, + numberRanges: tenants.numberRanges, + extraModules: tenants.extraModules, + }) + .from(authTenantUsers) + .innerJoin( + tenants, + eq(authTenantUsers.tenant_id, tenants.id) + ) + .where(eq(authTenantUsers.user_id, user_id)); + + return { + user_id, + tenants: tenantRecords, + }; + + } catch (err) { + console.error("ERROR /admin/user-tenants:", err); + return reply.code(500).send({ error: "Internal Server Error" }); + } + }); + +} diff --git a/src/routes/auth/auth-authenticated.ts b/src/routes/auth/auth-authenticated.ts new file mode 100644 index 0000000..b7626c0 --- /dev/null +++ b/src/routes/auth/auth-authenticated.ts @@ -0,0 +1,96 @@ +import { FastifyInstance } from "fastify" +import bcrypt from "bcrypt" +import { eq } from "drizzle-orm" + +import { authUsers } from "../../../db/schema" // wichtig: Drizzle Schema importieren! + +export default async function authRoutesAuthenticated(server: FastifyInstance) { + + server.post("/auth/password/change", { + schema: { + tags: ["Auth"], + summary: "Change password (after login or forced reset)", + body: { + type: "object", + required: ["old_password", "new_password"], + properties: { + old_password: { type: "string" }, + new_password: { type: "string" }, + }, + }, + response: { + 200: { + type: "object", + properties: { + success: { type: "boolean" }, + }, + }, + }, + }, + }, async (req, reply) => { + + try { + const { old_password, new_password } = req.body as { + old_password: string + new_password: string + } + + const userId = req.user?.user_id + if (!userId) { + //@ts-ignore + return reply.code(401).send({ error: "Unauthorized" }) + } + + // ----------------------------------------------------- + // 1) User laden + // ----------------------------------------------------- + const [user] = await server.db + .select({ + id: authUsers.id, + passwordHash: authUsers.passwordHash, + mustChangePassword: authUsers.must_change_password + }) + .from(authUsers) + .where(eq(authUsers.id, userId)) + .limit(1) + + if (!user) { + //@ts-ignore + return reply.code(404).send({ error: "User not found" }) + } + + // ----------------------------------------------------- + // 2) Altes PW prüfen + // ----------------------------------------------------- + const valid = await bcrypt.compare(old_password, user.passwordHash) + if (!valid) { + //@ts-ignore + return reply.code(401).send({ error: "Old password incorrect" }) + } + + // ----------------------------------------------------- + // 3) Neues PW hashen + // ----------------------------------------------------- + const newHash = await bcrypt.hash(new_password, 10) + + // ----------------------------------------------------- + // 4) Updaten + // ----------------------------------------------------- + await server.db + .update(authUsers) + .set({ + passwordHash: newHash, + must_change_password: false, + updatedAt: new Date(), + }) + .where(eq(authUsers.id, userId)) + + return { success: true } + + } catch (err) { + console.error("POST /auth/password/change ERROR:", err) + //@ts-ignore + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) +} diff --git a/src/routes/auth/auth.ts b/src/routes/auth/auth.ts new file mode 100644 index 0000000..dd4eb14 --- /dev/null +++ b/src/routes/auth/auth.ts @@ -0,0 +1,224 @@ +import { FastifyInstance } from "fastify"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; +import { generateRandomPassword, hashPassword } from "../../utils/password"; +import { sendMail } from "../../utils/mailer"; +import { secrets } from "../../utils/secrets"; + +import { authUsers } from "../../../db/schema"; +import { authTenantUsers } from "../../../db/schema"; +import { tenants } from "../../../db/schema"; +import { eq, and } from "drizzle-orm"; + +export default async function authRoutes(server: FastifyInstance) { + + // ----------------------------------------------------- + // REGISTER + // ----------------------------------------------------- + server.post("/auth/register", { + schema: { + tags: ["Auth"], + summary: "Register User", + body: { + type: "object", + required: ["email", "password"], + properties: { + email: { type: "string", format: "email" }, + password: { type: "string" }, + }, + }, + }, + }, async (req, reply) => { + const body = req.body as { email: string; password: string }; + + const passwordHash = await bcrypt.hash(body.password, 10); + + const [user] = await server.db + .insert(authUsers) + .values({ + email: body.email.toLowerCase(), + passwordHash, + }) + .returning({ + id: authUsers.id, + email: authUsers.email, + }); + + return { user }; + }); + + + // ----------------------------------------------------- + // LOGIN + // ----------------------------------------------------- + server.post("/auth/login", { + schema: { + tags: ["Auth"], + summary: "Login User", + body: { + type: "object", + required: ["email", "password"], + properties: { + email: { type: "string", format: "email" }, + password: { type: "string" }, + }, + }, + }, + }, async (req, reply) => { + const body = req.body as { email: string; password: string }; + + let user: any = null; + + // ------------------------------- + // SINGLE TENANT MODE + // ------------------------------- + /* if (req.tenant) { + const tenantId = req.tenant.id; + + const result = await server.db + .select({ + user: authUsers, + }) + .from(authUsers) + .innerJoin( + authTenantUsers, + eq(authTenantUsers.userId, authUsers.id) + ) + .innerJoin( + tenants, + eq(authTenantUsers.tenantId, tenants.id) + ) + .where(and( + eq(authUsers.email, body.email.toLowerCase()), + eq(authTenantUsers.tenantId, tenantId) + )); + + if (result.length === 0) { + return reply.code(401).send({ error: "Invalid credentials" }); + } + + user = result[0].user; + + // ------------------------------- + // MULTI TENANT MODE + // ------------------------------- + } else {*/ + const [found] = await server.db + .select() + .from(authUsers) + .where(eq(authUsers.email, body.email.toLowerCase())) + .limit(1); + + if (!found) { + return reply.code(401).send({ error: "Invalid credentials" }); + } + + user = found; + /*}*/ + + // Passwort prüfen + const valid = await bcrypt.compare(body.password, user.passwordHash); + if (!valid) { + return reply.code(401).send({ error: "Invalid credentials" }); + } + + const token = jwt.sign( + { + user_id: user.id, + email: user.email, + tenant_id: req.tenant?.id ?? null, + }, + secrets.JWT_SECRET!, + { expiresIn: "6h" } + ); + + reply.setCookie("token", token, { + path: "/", + httpOnly: true, + sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", + secure: process.env.NODE_ENV === "production", + maxAge: 60 * 60 * 3, + }); + + return { token }; + }); + + + // ----------------------------------------------------- + // LOGOUT + // ----------------------------------------------------- + server.post("/auth/logout", { + schema: { + tags: ["Auth"], + summary: "Logout User" + } + }, async (req, reply) => { + reply.clearCookie("token", { + path: "/", + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + }); + + return { success: true }; + }); + + + // ----------------------------------------------------- + // PASSWORD RESET + // ----------------------------------------------------- + server.post("/auth/password/reset", { + schema: { + tags: ["Auth"], + summary: "Reset Password", + body: { + type: "object", + required: ["email"], + properties: { + email: { type: "string", format: "email" } + } + } + } + }, async (req, reply) => { + const { email } = req.body as { email: string }; + + const [user] = await server.db + .select({ + id: authUsers.id, + email: authUsers.email, + }) + .from(authUsers) + .where(eq(authUsers.email, email.toLowerCase())) + .limit(1); + + if (!user) { + return reply.code(404).send({ error: "User not found" }); + } + + const plainPassword = generateRandomPassword(); + const passwordHash = await hashPassword(plainPassword); + + + await server.db + .update(authUsers) + .set({ + passwordHash, + // @ts-ignore + mustChangePassword: true, + }) + .where(eq(authUsers.id, user.id)); + + await sendMail( + user.email, + "FEDEO | Dein neues Passwort", + ` +

Hallo,

+

Dein Passwort wurde zurückgesetzt.

+

Neues Passwort: ${plainPassword}

+

Bitte ändere es nach dem Login umgehend.

+ ` + ); + + return { success: true }; + }); +} diff --git a/src/routes/auth/me.ts b/src/routes/auth/me.ts new file mode 100644 index 0000000..fffb793 --- /dev/null +++ b/src/routes/auth/me.ts @@ -0,0 +1,140 @@ +import { FastifyInstance } from "fastify" +import { + authUsers, + authTenantUsers, + tenants, + authProfiles, + authUserRoles, + authRoles, + authRolePermissions, +} from "../../../db/schema" +import { eq, and, or, isNull } from "drizzle-orm" + +export default async function meRoutes(server: FastifyInstance) { + server.get("/me", async (req, reply) => { + try { + const authUser = req.user + + if (!authUser) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + const userId = authUser.user_id + const activeTenantId = authUser.tenant_id + + // ---------------------------------------------------- + // 1) USER LADEN + // ---------------------------------------------------- + const userResult = await server.db + .select({ + id: authUsers.id, + email: authUsers.email, + created_at: authUsers.created_at, + must_change_password: authUsers.must_change_password, + }) + .from(authUsers) + .where(eq(authUsers.id, userId)) + .limit(1) + + const user = userResult[0] + + if (!user) { + return reply.code(401).send({ error: "User not found" }) + } + + // ---------------------------------------------------- + // 2) TENANTS LADEN + // ---------------------------------------------------- + const tenantRows = await server.db + .select({ + id: tenants.id, + name: tenants.name, + short: tenants.short, + locked: tenants.locked, + extraModules: tenants.extraModules, + businessInfo: tenants.businessInfo, + numberRanges: tenants.numberRanges, + dokuboxkey: tenants.dokuboxkey, + standardEmailForInvoices: tenants.standardEmailForInvoices, + standardPaymentDays: tenants.standardPaymentDays, + }) + .from(authTenantUsers) + .innerJoin(tenants, eq(authTenantUsers.tenant_id, tenants.id)) + .where(eq(authTenantUsers.user_id, userId)) + + const tenantList = tenantRows ?? [] + + // ---------------------------------------------------- + // 3) ACTIVE TENANT + // ---------------------------------------------------- + const activeTenant = activeTenantId + + // ---------------------------------------------------- + // 4) PROFIL LADEN + // ---------------------------------------------------- + let profile = null + if (activeTenantId) { + const profileResult = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.user_id, userId), + eq(authProfiles.tenant_id, activeTenantId) + ) + ) + .limit(1) + + profile = profileResult?.[0] ?? null + } + + // ---------------------------------------------------- + // 5) PERMISSIONS — RPC ERSETZT + // ---------------------------------------------------- + const permissionRows = + (await server.db + .select({ + permission: authRolePermissions.permission, + }) + .from(authUserRoles) + .innerJoin( + authRoles, + and( + eq(authRoles.id, authUserRoles.role_id), + or( + isNull(authRoles.tenant_id), // globale Rolle + eq(authRoles.tenant_id, activeTenantId) // tenant-spezifische Rolle + ) + ) + ) + .innerJoin( + authRolePermissions, + eq(authRolePermissions.role_id, authRoles.id) + ) + .where( + and( + eq(authUserRoles.user_id, userId), + eq(authUserRoles.tenant_id, activeTenantId) + ) + )) ?? [] + + const permissions = Array.from( + new Set(permissionRows.map((p) => p.permission)) + ) + + // ---------------------------------------------------- + // RESPONSE + // ---------------------------------------------------- + return { + user, + tenants: tenantList, + activeTenant, + profile, + permissions, + } + } catch (err: any) { + console.error("ERROR in /me route:", err) + return reply.code(500).send({ error: "Internal server error" }) + } + }) +} diff --git a/src/routes/auth/user.ts b/src/routes/auth/user.ts new file mode 100644 index 0000000..fde299a --- /dev/null +++ b/src/routes/auth/user.ts @@ -0,0 +1,129 @@ +import { FastifyInstance } from "fastify" +import { eq, and } from "drizzle-orm" + +import { + authUsers, + authProfiles, +} from "../../../db/schema" + +export default async function userRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // GET /user/:id + // ------------------------------------------------------------- + server.get("/user/:id", async (req, reply) => { + try { + const authUser = req.user + const { id } = req.params as { id: string } + + if (!authUser) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + // 1️⃣ User laden + const [user] = await server.db + .select({ + id: authUsers.id, + email: authUsers.email, + created_at: authUsers.created_at, + must_change_password: authUsers.must_change_password, + }) + .from(authUsers) + .where(eq(authUsers.id, id)) + + if (!user) { + return reply.code(404).send({ error: "User not found" }) + } + + // 2️⃣ Profil im Tenant + let profile = null + + if (authUser.tenant_id) { + const [profileRow] = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.user_id, id), + eq(authProfiles.tenant_id, authUser.tenant_id) + ) + ) + + profile = profileRow || null + } + + return { user, profile } + + } catch (err: any) { + console.error("/user/:id ERROR", err) + return reply.code(500).send({ error: err.message || "Internal error" }) + } + }) + + // ------------------------------------------------------------- + // PUT /user/:id/profile + // ------------------------------------------------------------- + server.put("/user/:id/profile", async (req, reply) => { + try { + const { id } = req.params as { id: string } + const { data } = req.body as { data?: Record } + + if (!req.user?.tenant_id) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + if (!data || typeof data !== "object") { + return reply.code(400).send({ error: "data object required" }) + } + + // 1️⃣ Profil für diesen Tenant laden (damit wir die ID kennen) + const [profile] = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.user_id, id), + eq(authProfiles.tenant_id, req.user.tenant_id) + ) + ) + + if (!profile) { + return reply.code(404).send({ error: "Profile not found in tenant" }) + } + + // 2️⃣ Timestamp-Felder normalisieren (falls welche drin sind) + const normalizeDate = (val: any) => { + if (!val) return null + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + const updateData: any = { ...data } + + // bekannte Date-Felder prüfen + if (data.entry_date !== undefined) + updateData.entry_date = normalizeDate(data.entry_date) + + if (data.birthday !== undefined) + updateData.birthday = normalizeDate(data.birthday) + + if (data.created_at !== undefined) + updateData.created_at = normalizeDate(data.created_at) + + updateData.updated_at = new Date() + + // 3️⃣ Update durchführen + const [updatedProfile] = await server.db + .update(authProfiles) + .set(updateData) + .where(eq(authProfiles.id, profile.id)) + .returning() + + return { profile: updatedProfile } + + } catch (err: any) { + console.error("PUT /user/:id/profile ERROR", err) + return reply.code(500).send({ error: err.message || "Internal server error" }) + } + }) +} diff --git a/src/routes/banking.ts b/src/routes/banking.ts new file mode 100644 index 0000000..f06ab60 --- /dev/null +++ b/src/routes/banking.ts @@ -0,0 +1,236 @@ +import { FastifyInstance } from "fastify" +import axios from "axios" +import dayjs from "dayjs" + +import { secrets } from "../utils/secrets" +import { insertHistoryItem } from "../utils/history" + +import { + bankrequisitions, + statementallocations, +} from "../../db/schema" + +import { + eq, + and, +} from "drizzle-orm" + + +export default async function bankingRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------------ + // 🔐 GoCardLess Token Handling + // ------------------------------------------------------------------ + + const goCardLessBaseUrl = secrets.GOCARDLESS_BASE_URL + const goCardLessSecretId = secrets.GOCARDLESS_SECRET_ID + const goCardLessSecretKey = secrets.GOCARDLESS_SECRET_KEY + + let tokenData: any = null + + const getToken = async () => { + const res = await axios.post(`${goCardLessBaseUrl}/token/new/`, { + secret_id: goCardLessSecretId, + secret_key: goCardLessSecretKey, + }) + + tokenData = res.data + tokenData.created_at = new Date().toISOString() + + server.log.info("GoCardless token refreshed.") + } + + const checkToken = async () => { + if (!tokenData) return await getToken() + + const expired = dayjs(tokenData.created_at) + .add(tokenData.access_expires, "seconds") + .isBefore(dayjs()) + + if (expired) { + server.log.info("Refreshing expired GoCardless token …") + await getToken() + } + } + + // ------------------------------------------------------------------ + // 🔗 Create GoCardless Banking Link + // ------------------------------------------------------------------ + server.get("/banking/link/:institutionid", async (req, reply) => { + try { + await checkToken() + + const { institutionid } = req.params as { institutionid: string } + const tenantId = req.user?.tenant_id + + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const { data } = await axios.post( + `${goCardLessBaseUrl}/requisitions/`, + { + redirect: "https://app.fedeo.de/settings/banking", + institution_id: institutionid, + user_language: "de", + }, + { + headers: { Authorization: `Bearer ${tokenData.access}` }, + } + ) + + // DB: Requisition speichern + await server.db.insert(bankrequisitions).values({ + id: data.id, + tenant: tenantId, + institutionId: institutionid, + status: data.status, + }) + + return reply.send({ link: data.link }) + } catch (err: any) { + server.log.error(err?.response?.data || err) + return reply.code(500).send({ error: "Failed to generate link" }) + } + }) + + // ------------------------------------------------------------------ + // 🏦 Check Bank Institutions + // ------------------------------------------------------------------ + server.get("/banking/institutions/:bic", async (req, reply) => { + try { + const { bic } = req.params as { bic: string } + if (!bic) return reply.code(400).send("BIC missing") + + await checkToken() + + const { data } = await axios.get( + `${goCardLessBaseUrl}/institutions/?country=de`, + { headers: { Authorization: `Bearer ${tokenData.access}` } } + ) + + const bank = data.find((i: any) => i.bic.toLowerCase() === bic.toLowerCase()) + + if (!bank) return reply.code(404).send("Bank not found") + + return reply.send(bank) + } catch (err: any) { + server.log.error(err?.response?.data || err) + return reply.code(500).send("Failed to fetch institutions") + } + }) + + + // ------------------------------------------------------------------ + // 📄 Get Requisition Details + // ------------------------------------------------------------------ + server.get("/banking/requisitions/:reqId", async (req, reply) => { + try { + const { reqId } = req.params as { reqId: string } + if (!reqId) return reply.code(400).send("Requisition ID missing") + + await checkToken() + + const { data } = await axios.get( + `${goCardLessBaseUrl}/requisitions/${reqId}`, + { headers: { Authorization: `Bearer ${tokenData.access}` } } + ) + + // Load account details + if (data.accounts) { + data.accounts = await Promise.all( + data.accounts.map(async (accId: string) => { + const { data: acc } = await axios.get( + `${goCardLessBaseUrl}/accounts/${accId}`, + { headers: { Authorization: `Bearer ${tokenData.access}` } } + ) + return acc + }) + ) + } + + return reply.send(data) + } catch (err: any) { + server.log.error(err?.response?.data || err) + return reply.code(500).send("Failed to fetch requisition details") + } + }) + + + // ------------------------------------------------------------------ + // 💰 Create Statement Allocation + // ------------------------------------------------------------------ + server.post("/banking/statements", async (req, reply) => { + try { + if (!req.user) return reply.code(401).send({ error: "Unauthorized" }) + + const { data: payload } = req.body as { data: any } + + const inserted = await server.db.insert(statementallocations).values({ + ...payload, + tenant: req.user.tenant_id + }).returning() + + const createdRecord = inserted[0] + + await insertHistoryItem(server, { + entity: "bankstatements", + entityId: createdRecord.id, + action: "created", + created_by: req.user.user_id, + tenant_id: req.user.tenant_id, + oldVal: null, + newVal: createdRecord, + text: "Buchung erstellt", + }) + + return reply.send(createdRecord) + + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Failed to create statement" }) + } + }) + + + // ------------------------------------------------------------------ + // 🗑 Delete Statement Allocation + // ------------------------------------------------------------------ + server.delete("/banking/statements/:id", async (req, reply) => { + try { + if (!req.user) return reply.code(401).send({ error: "Unauthorized" }) + + const { id } = req.params as { id: string } + + const oldRecord = await server.db + .select() + .from(statementallocations) + .where(eq(statementallocations.id, id)) + .limit(1) + + const old = oldRecord[0] + + if (!old) return reply.code(404).send({ error: "Record not found" }) + + await server.db + .delete(statementallocations) + .where(eq(statementallocations.id, id)) + + await insertHistoryItem(server, { + entity: "bankstatements", + entityId: id, + action: "deleted", + created_by: req.user.user_id, + tenant_id: req.user.tenant_id, + oldVal: old, + newVal: null, + text: "Buchung gelöscht", + }) + + return reply.send({ success: true }) + + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Failed to delete statement" }) + } + }) + +} diff --git a/src/routes/devices/rfid.ts b/src/routes/devices/rfid.ts new file mode 100644 index 0000000..2931c07 --- /dev/null +++ b/src/routes/devices/rfid.ts @@ -0,0 +1,98 @@ +import { FastifyInstance } from "fastify"; +import {and, desc, eq} from "drizzle-orm"; +import {authProfiles, devices, stafftimeevents} from "../../../db/schema"; + +export default async function devicesRFIDRoutes(server: FastifyInstance) { + server.post( + "/rfid/createevent/:terminal_id", + async (req, reply) => { + try { + + const {rfid_id} = req.body as {rfid_id: string}; + const {terminal_id} = req.params as {terminal_id: string}; + + if(!rfid_id ||!terminal_id) { + console.log(`Missing Params`); + return reply.code(400).send(`Missing Params`) + } + + const device = await server.db + .select() + .from(devices) + .where( + eq(devices.externalId, terminal_id) + + ) + .limit(1) + .then(rows => rows[0]); + + if(!device) { + console.log(`Device ${terminal_id} not found`); + return reply.code(400).send(`Device ${terminal_id} not found`) + + } + + const profile = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.tenant_id, device.tenant), + eq(authProfiles.token_id, rfid_id) + ) + ) + .limit(1) + .then(rows => rows[0]); + + if(!profile) { + console.log(`Profile for Token ${rfid_id} not found`); + return reply.code(400).send(`Profile for Token ${rfid_id} not found`) + + } + + const lastEvent = await server.db + .select() + .from(stafftimeevents) + .where( + eq(stafftimeevents.user_id, profile.user_id) + ) + .orderBy(desc(stafftimeevents.eventtime)) // <-- Sortierung: Neuestes zuerst + .limit(1) + .then(rows => rows[0]); + + console.log(lastEvent) + + + const dataToInsert = { + tenant_id: device.tenant, + user_id: profile.user_id, + actortype: "system", + eventtime: new Date(), + eventtype: lastEvent.eventtype === "work_start" ? "work_end" : "work_start", + source: "WEB" + } + + console.log(dataToInsert) + + const [created] = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(dataToInsert) + .returning() + + return created + } catch (err: any) { + console.error(err) + return reply.code(400).send({ error: err.message }) + } + + + + console.log(req.body) + + return + + + } + ); +} diff --git a/src/routes/emailAsUser.ts b/src/routes/emailAsUser.ts new file mode 100644 index 0000000..c2d147a --- /dev/null +++ b/src/routes/emailAsUser.ts @@ -0,0 +1,262 @@ +import nodemailer from "nodemailer" +import { FastifyInstance } from "fastify" +import { eq } from "drizzle-orm" + +import { sendMailAsUser } from "../utils/emailengine" +import { encrypt, decrypt } from "../utils/crypt" +import { userCredentials } from "../../db/schema" +// Pfad ggf. anpassen + +// @ts-ignore +import MailComposer from "nodemailer/lib/mail-composer/index.js" +import { ImapFlow } from "imapflow" + +export default async function emailAsUserRoutes(server: FastifyInstance) { + + + // ====================================================================== + // CREATE OR UPDATE EMAIL ACCOUNT + // ====================================================================== + server.post("/email/accounts/:id?", async (req, reply) => { + try { + if (!req.user?.tenant_id) { + return reply.code(400).send({ error: "No tenant selected" }) + } + + const { id } = req.params as { id?: string } + + const body = req.body as { + email: string + password: string + smtp_host: string + smtp_port: number + smtp_ssl: boolean + imap_host: string + imap_port: number + imap_ssl: boolean + } + + // ----------------------------- + // UPDATE EXISTING + // ----------------------------- + if (id) { + const saveData = { + emailEncrypted: body.email ? encrypt(body.email) : undefined, + passwordEncrypted: body.password ? encrypt(body.password) : undefined, + smtpHostEncrypted: body.smtp_host ? encrypt(body.smtp_host) : undefined, + smtpPort: body.smtp_port, + smtpSsl: body.smtp_ssl, + imapHostEncrypted: body.imap_host ? encrypt(body.imap_host) : undefined, + imapPort: body.imap_port, + imapSsl: body.imap_ssl, + } + + await server.db + .update(userCredentials) + //@ts-ignore + .set(saveData) + .where(eq(userCredentials.id, id)) + + return reply.send({ success: true }) + } + + // ----------------------------- + // CREATE NEW + // ----------------------------- + const insertData = { + userId: req.user.user_id, + tenantId: req.user.tenant_id, + type: "mail", + + emailEncrypted: encrypt(body.email), + passwordEncrypted: encrypt(body.password), + + smtpHostEncrypted: encrypt(body.smtp_host), + smtpPort: body.smtp_port, + smtpSsl: body.smtp_ssl, + + imapHostEncrypted: encrypt(body.imap_host), + imapPort: body.imap_port, + imapSsl: body.imap_ssl, + } + + //@ts-ignore + await server.db.insert(userCredentials).values(insertData) + + return reply.send({ success: true }) + } catch (err) { + console.error("POST /email/accounts error:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ====================================================================== + // GET SINGLE OR ALL ACCOUNTS + // ====================================================================== + server.get("/email/accounts/:id?", async (req, reply) => { + try { + if (!req.user?.tenant_id) { + return reply.code(400).send({ error: "No tenant selected" }) + } + + const { id } = req.params as { id?: string } + + // ============================================================ + // LOAD SINGLE ACCOUNT + // ============================================================ + if (id) { + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.id, id)) + + const row = rows[0] + if (!row) return reply.code(404).send({ error: "Not found" }) + + const returnData: any = {} + + Object.entries(row).forEach(([key, val]) => { + if (key.endsWith("Encrypted")) { + const cleanKey = key.replace("Encrypted", "") + // @ts-ignore + returnData[cleanKey] = decrypt(val as string) + } else { + returnData[key] = val + } + }) + + return reply.send(returnData) + } + + // ============================================================ + // LOAD ALL ACCOUNTS FOR TENANT + // ============================================================ + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.tenantId, req.user.tenant_id)) + + const accounts = rows.map(row => { + const temp: any = {} + console.log(row) + Object.entries(row).forEach(([key, val]) => { + console.log(key,val) + if (key.endsWith("Encrypted") && val) { + // @ts-ignore + temp[key.replace("Encrypted", "")] = decrypt(val) + } else { + temp[key] = val + } + }) + return temp + }) + + return reply.send(accounts) + + } catch (err) { + console.error("GET /email/accounts error:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ====================================================================== + // SEND EMAIL + SAVE IN IMAP SENT FOLDER + // ====================================================================== + server.post("/email/send", async (req, reply) => { + try { + const body = req.body as { + to: string + cc?: string + bcc?: string + subject?: string + text?: string + html?: string + attachments?: any + account: string + } + + // Fetch email credentials + const rows = await server.db + .select() + .from(userCredentials) + .where(eq(userCredentials.id, body.account)) + + const row = rows[0] + if (!row) return reply.code(404).send({ error: "Account not found" }) + + const accountData: any = {} + + Object.entries(row).forEach(([key, val]) => { + if (key.endsWith("Encrypted") && val) { + // @ts-ignore + accountData[key.replace("Encrypted", "")] = decrypt(val as string) + } else { + accountData[key] = val + } + }) + + // ------------------------- + // SEND EMAIL VIA SMTP + // ------------------------- + const transporter = nodemailer.createTransport({ + host: accountData.smtpHost, + port: accountData.smtpPort, + secure: accountData.smtpSsl, + auth: { + user: accountData.email, + pass: accountData.password, + }, + }) + + const message = { + from: accountData.email, + to: body.to, + cc: body.cc, + bcc: body.bcc, + subject: body.subject, + html: body.html, + text: body.text, + attachments: body.attachments, + } + + const info = await transporter.sendMail(message) + + // ------------------------- + // SAVE TO IMAP SENT FOLDER + // ------------------------- + const imap = new ImapFlow({ + host: accountData.imapHost, + port: accountData.imapPort, + secure: accountData.imapSsl, + auth: { + user: accountData.email, + pass: accountData.password, + }, + }) + + await imap.connect() + + const mail = new MailComposer(message) + const raw = await mail.compile().build() + + for await (const mailbox of await imap.list()) { + if (mailbox.specialUse === "\\Sent") { + await imap.mailboxOpen(mailbox.path) + await imap.append(mailbox.path, raw, ["\\Seen"]) + await imap.logout() + } + } + + return reply.send({ success: true }) + + } catch (err) { + console.error("POST /email/send error:", err) + return reply.code(500).send({ error: "Failed to send email" }) + } + }) + +} diff --git a/src/routes/exports.ts b/src/routes/exports.ts new file mode 100644 index 0000000..5aff4ba --- /dev/null +++ b/src/routes/exports.ts @@ -0,0 +1,128 @@ +import { FastifyInstance } from "fastify"; +import jwt from "jsonwebtoken"; +import {insertHistoryItem} from "../utils/history"; +import {buildExportZip} from "../utils/export/datev"; +import {s3} from "../utils/s3"; +import {GetObjectCommand, PutObjectCommand} from "@aws-sdk/client-s3" +import {getSignedUrl} from "@aws-sdk/s3-request-presigner"; +import dayjs from "dayjs"; +import {randomUUID} from "node:crypto"; +import {secrets} from "../utils/secrets"; +import {createSEPAExport} from "../utils/export/sepa"; + +const createDatevExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => { + console.log(startDate,endDate,beraternr,mandantennr) + + // 1) ZIP erzeugen + const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr) + console.log("ZIP created") + console.log(buffer) + + // 2) Dateiname & Key festlegen + const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip` + console.log(fileKey) + + // 3) In S3 hochladen + await s3.send( + new PutObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: fileKey, + Body: buffer, + ContentType: "application/zip", + }) + ) + + // 4) Presigned URL erzeugen (24h gültig) + const url = await getSignedUrl( + s3, + new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: fileKey, + }), + { expiresIn: 60 * 60 * 24 } + ) + + console.log(url) + + // 5) In Supabase-DB speichern + const { data, error } = await server.supabase + .from("exports") + .insert([ + { + tenant_id: req.user.tenant_id, + start_date: startDate, + end_date: endDate, + valid_until: dayjs().add(24,"hours").toISOString(), + file_path: fileKey, + url: url, + created_at: new Date().toISOString(), + }, + ]) + .select() + .single() + + console.log(data) + console.log(error) +} + + +export default async function exportRoutes(server: FastifyInstance) { + //Export DATEV + server.post("/exports/datev", async (req, reply) => { + const { start_date, end_date, beraternr, mandantennr } = req.body as { + start_date: string + end_date: string + beraternr: string + mandantennr: string + } + + + + reply.send({success:true}) + + setImmediate(async () => { + try { + await createDatevExport(server,req,start_date,end_date,beraternr,mandantennr) + console.log("Job done ✅") + } catch (err) { + console.error("Job failed ❌", err) + } + }) + + }) + + server.post("/exports/sepa", async (req, reply) => { + const { idsToExport } = req.body as { + idsToExport: Array + } + + + + reply.send({success:true}) + + setImmediate(async () => { + try { + await createSEPAExport(server, idsToExport, req.user.tenant_id) + console.log("Job done ✅") + } catch (err) { + console.error("Job failed ❌", err) + } + }) + + }) + + //List Exports Available for Download + + server.get("/exports", async (req,reply) => { + const {data,error} = await server.supabase.from("exports").select().eq("tenant_id",req.user.tenant_id) + + console.log(data,error) + reply.send(data) + + }) + + + + + +} \ No newline at end of file diff --git a/src/routes/files.ts b/src/routes/files.ts new file mode 100644 index 0000000..5c7ac9d --- /dev/null +++ b/src/routes/files.ts @@ -0,0 +1,293 @@ +import { FastifyInstance } from "fastify" +import multipart from "@fastify/multipart" +import { s3 } from "../utils/s3" +import { + GetObjectCommand, + PutObjectCommand +} from "@aws-sdk/client-s3" +import { getSignedUrl } from "@aws-sdk/s3-request-presigner" +import archiver from "archiver" +import { secrets } from "../utils/secrets" + +import { eq, inArray } from "drizzle-orm" +import { + files, + createddocuments, + customers +} from "../../db/schema" + + +export default async function fileRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // MULTIPART INIT + // ------------------------------------------------------------- + await server.register(multipart, { + limits: { fileSize: 20 * 1024 * 1024 } // 20 MB + }) + + + // ------------------------------------------------------------- + // UPLOAD FILE + // ------------------------------------------------------------- + server.post("/files/upload", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const data: any = await req.file() + if (!data?.file) return reply.code(400).send({ error: "No file uploaded" }) + const fileBuffer = await data.toBuffer() + + const meta = data.fields?.meta?.value ? JSON.parse(data.fields.meta.value) : {} + + // 1️⃣ DB-Eintrag erzeugen + const inserted = await server.db + .insert(files) + .values({ tenant: tenantId }) + .returning() + + const created = inserted[0] + if (!created) throw new Error("Could not create DB entry") + + // 2️⃣ Datei in S3 speichern + const fileKey = `${tenantId}/filesbyid/${created.id}/${data.filename}` + + await s3.send(new PutObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: fileKey, + Body: fileBuffer, + ContentType: data.mimetype + })) + + // 3️⃣ DB updaten: meta + path + await server.db + .update(files) + .set({ + ...meta, + path: fileKey + }) + .where(eq(files.id, created.id)) + + return { + id: created.id, + filename: data.filename, + path: fileKey + } + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Upload failed" }) + } + }) + + + + // ------------------------------------------------------------- + // GET FILE OR LIST FILES + // ------------------------------------------------------------- + server.get("/files/:id?", async (req, reply) => { + try { + const { id } = req.params as { id?: string } + + // 🔹 EINZELNE DATEI + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) + + const file = rows[0] + if (!file) return reply.code(404).send({ error: "Not found" }) + + return file + } + + // 🔹 ALLE DATEIEN DES TENANTS (mit createddocument + customer) + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const list = await server.db + //@ts-ignore + .select({ + ...files, + createddocument: createddocuments, + customer: customers + }) + .from(files) + .leftJoin( + createddocuments, + eq(files.createddocument, createddocuments.id) + ) + .leftJoin( + customers, + eq(createddocuments.customer, customers.id) + ) + .where(eq(files.tenant, tenantId)) + + return { files: list } + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Could not load files" }) + } + }) + + + + // ------------------------------------------------------------- + // DOWNLOAD (SINGLE OR MULTI ZIP) + // ------------------------------------------------------------- + server.post("/files/download/:id?", async (req, reply) => { + try { + const { id } = req.params as { id?: string } + //@ts-ignore + const ids = req.body?.ids || [] + + // ------------------------------------------------- + // 1️⃣ SINGLE DOWNLOAD + // ------------------------------------------------- + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) + + const file = rows[0] + if (!file) return reply.code(404).send({ error: "File not found" }) + + const command = new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: file.path! + }) + + const { Body, ContentType } = await s3.send(command) + + const chunks: any[] = [] + for await (const chunk of Body as any) chunks.push(chunk) + const buffer = Buffer.concat(chunks) + + reply.header("Content-Type", ContentType || "application/octet-stream") + reply.header("Content-Disposition", `attachment; filename="${file.path?.split("/").pop()}"`) + return reply.send(buffer) + } + + + // ------------------------------------------------- + // 2️⃣ MULTI DOWNLOAD → ZIP + // ------------------------------------------------- + if (Array.isArray(ids) && ids.length > 0) { + const rows = await server.db + .select() + .from(files) + .where(inArray(files.id, ids)) + + if (!rows.length) return reply.code(404).send({ error: "Files not found" }) + + reply.header("Content-Type", "application/zip") + reply.header("Content-Disposition", `attachment; filename="dateien.zip"`) + + const archive = archiver("zip", { zlib: { level: 9 } }) + + for (const entry of rows) { + const cmd = new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: entry.path! + }) + const { Body } = await s3.send(cmd) + + archive.append(Body as any, { + name: entry.path?.split("/").pop() || entry.id + }) + } + + await archive.finalize() + return reply.send(archive) + } + + return reply.code(400).send({ error: "No id or ids provided" }) + + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Download failed" }) + } + }) + + + + // ------------------------------------------------------------- + // GENERATE PRESIGNED URL(S) + // ------------------------------------------------------------- + server.post("/files/presigned/:id?", async (req, reply) => { + try { + const { id } = req.params as { id?: string } + const { ids } = req.body as { ids?: string[] } + const tenantId = req.user?.tenant_id + + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + // ------------------------------------------------- + // SINGLE FILE PRESIGNED URL + // ------------------------------------------------- + if (id) { + const rows = await server.db + .select() + .from(files) + .where(eq(files.id, id)) + + const file = rows[0] + if (!file) return reply.code(404).send({ error: "Not found" }) + + const url = await getSignedUrl( + s3, + new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), + { expiresIn: 900 } + ) + + return { ...file, url } + } else { + // ------------------------------------------------- + // MULTIPLE PRESIGNED URLs + // ------------------------------------------------- + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return reply.code(400).send({ error: "No ids provided" }) + } + + const rows = await server.db + .select() + .from(files) + .where(eq(files.tenant, tenantId)) + + const selected = rows.filter(f => ids.includes(f.id) && f.path) + + console.log(selected) + + const url = await getSignedUrl( + s3, + new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: selected[0].path! }), + { expiresIn: 900 } + ) + console.log(url) + console.log(selected.filter(f => !f.path)) + + const output = await Promise.all( + selected.map(async (file) => { + const url = await getSignedUrl( + s3, + new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: file.path! }), + { expiresIn: 900 } + ) + return { ...file, url } + }) + ) + + return { files: output } + } + + + + } catch (err) { + console.error(err) + return reply.code(500).send({ error: "Could not create presigned URLs" }) + } + }) + +} diff --git a/src/routes/functions.ts b/src/routes/functions.ts new file mode 100644 index 0000000..d37963b --- /dev/null +++ b/src/routes/functions.ts @@ -0,0 +1,222 @@ +import { FastifyInstance } from "fastify"; +import {createInvoicePDF, createTimeSheetPDF} from "../utils/pdf"; +//import {encodeBase64ToNiimbot, generateLabel, useNextNumberRangeNumber} from "../utils/functions"; +import dayjs from "dayjs"; +//import { ready as zplReady } from 'zpl-renderer-js' +//import { renderZPL } from "zpl-image"; + +import customParseFormat from "dayjs/plugin/customParseFormat.js"; +import isoWeek from "dayjs/plugin/isoWeek.js"; +import isBetween from "dayjs/plugin/isBetween.js"; +import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js" +import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js" +import duration from "dayjs/plugin/duration.js"; +import timezone from "dayjs/plugin/timezone.js"; +import {generateTimesEvaluation} from "../modules/time/evaluation.service"; +import {citys} from "../../db/schema"; +import {eq} from "drizzle-orm"; +import {useNextNumberRangeNumber} from "../utils/functions"; +import {executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service"; +dayjs.extend(customParseFormat) +dayjs.extend(isoWeek) +dayjs.extend(isBetween) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) +dayjs.extend(duration) +dayjs.extend(timezone) + +export default async function functionRoutes(server: FastifyInstance) { + server.post("/functions/pdf/:type", async (req, reply) => { + const body = req.body as { + data: any + backgroundPath?: string + } + const {type} = req.params as {type:string} + + + try { + + let pdf = null + + if(type === "createdDocument") { + pdf = await createInvoicePDF( + server, + "base64", + body.data, + body.backgroundPath + ) + } else if(type === "timesheet") { + pdf = await createTimeSheetPDF( + server, + "base64", + body.data, + body.backgroundPath + ) + } + + return pdf // Fastify wandelt automatisch in JSON + } catch (err) { + console.log(err) + reply.code(500).send({ error: "Failed to create PDF" }) + } + }) + + server.get("/functions/usenextnumber/:numberrange", async (req, reply) => { + const { numberrange } = req.params as { numberrange: string }; + const tenant = (req as any).user.tenant_id + + try { + const result = await useNextNumberRangeNumber(server,tenant, numberrange) + reply.send(result) // JSON automatisch + } catch (err) { + req.log.error(err) + reply.code(500).send({ error: "Failed to generate next number" }) + } + }) + + /** + * @route GET /functions/workingtimeevaluation/:user_id + * @query start_date=YYYY-MM-DD + * @query end_date=YYYY-MM-DD + */ + server.get("/functions/timeevaluation/:user_id", async (req, reply) => { + const { user_id } = req.params as { user_id: string } + const { start_date, end_date } = req.query as { start_date: string; end_date: string } + const { tenant_id } = req.user + + // 🔒 Sicherheitscheck: andere User nur bei Berechtigung + if (user_id !== req.user.user_id && !req.hasPermission("staff.time.read_all")) { + return reply.code(403).send({ error: "Not allowed to view other users." }) + } + + try { + const result = await generateTimesEvaluation(server, user_id, tenant_id, start_date, end_date) + reply.send(result) + } catch (error) { + console.error(error) + reply.code(500).send({ error: error.message }) + } + }) + + server.get('/functions/check-zip/:zip', async (req, reply) => { + const { zip } = req.params as { zip: string } + + if (!zip) { + return reply.code(400).send({ error: 'ZIP is required' }) + } + + try { + //@ts-ignore + const data = await server.db.select().from(citys).where(eq(citys.zip,zip)) + + + /*const { data, error } = await server.supabase + .from('citys') + .select() + .eq('zip', zip) + .maybeSingle() + + if (error) { + console.log(error) + return reply.code(500).send({ error: 'Database error' }) + }*/ + + if (!data) { + return reply.code(404).send({ error: 'ZIP not found' }) + } + + //districtMap + const bundeslaender = [ + { code: 'DE-BW', name: 'Baden-Württemberg' }, + { code: 'DE-BY', name: 'Bayern' }, + { code: 'DE-BE', name: 'Berlin' }, + { code: 'DE-BB', name: 'Brandenburg' }, + { code: 'DE-HB', name: 'Bremen' }, + { code: 'DE-HH', name: 'Hamburg' }, + { code: 'DE-HE', name: 'Hessen' }, + { code: 'DE-MV', name: 'Mecklenburg-Vorpommern' }, + { code: 'DE-NI', name: 'Niedersachsen' }, + { code: 'DE-NW', name: 'Nordrhein-Westfalen' }, + { code: 'DE-RP', name: 'Rheinland-Pfalz' }, + { code: 'DE-SL', name: 'Saarland' }, + { code: 'DE-SN', name: 'Sachsen' }, + { code: 'DE-ST', name: 'Sachsen-Anhalt' }, + { code: 'DE-SH', name: 'Schleswig-Holstein' }, + { code: 'DE-TH', name: 'Thüringen' } + ] + + + + return reply.send({ + ...data, + //@ts-ignore + state_code: bundeslaender.find(i => i.name === data.countryName) + }) + } catch (err) { + console.log(err) + return reply.code(500).send({ error: 'Internal server error' }) + } + }) + + server.post('/functions/serial/start', async (req, reply) => { + console.log(req.body) + const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number} + await executeManualGeneration(server,executionDate,templateIds,tenantId,req.user.user_id) + }) + + server.post('/functions/serial/finish/:execution_id', async (req, reply) => { + const {execution_id} = req.params as { execution_id: string } + //@ts-ignore + await finishManualGeneration(server,execution_id) + }) + + server.post('/functions/services/bankstatementsync', async (req, reply) => { + await server.services.bankStatements.run(req.user.tenant_id); + }) + + server.post('/functions/services/prepareincominginvoices', async (req, reply) => { + + await server.services.prepareIncomingInvoices.run(req.user.tenant_id) + }) + + + /*server.post('/print/zpl/preview', async (req, reply) => { + const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string} + + console.log(widthMm,heightMm,dpmm) + + if (!zpl) { + return reply.code(400).send({ error: 'Missing ZPL string' }) + } + + try { + // 1️⃣ Renderer initialisieren + const { api } = await zplReady + + // 2️⃣ Rendern (liefert base64-encoded PNG) + const base64Png = await api.zplToBase64Async(zpl, widthMm, heightMm, dpmm) + + return await encodeBase64ToNiimbot(base64Png, 'top') + } catch (err) { + console.error('[ZPL Preview Error]', err) + return reply.code(500).send({ error: err.message || 'Failed to render ZPL' }) + } + }) + + server.post('/print/label', async (req, reply) => { + const { context, width=584, heigth=354 } = req.body as {context:any,width:number,heigth:number} + + try { + const base64 = await generateLabel(context,width,heigth) + + return { + encoded: await encodeBase64ToNiimbot(base64, 'top'), + base64: base64 + } + } catch (err) { + console.error('[ZPL Preview Error]', err) + return reply.code(500).send({ error: err.message || 'Failed to render ZPL' }) + } + })*/ + +} \ No newline at end of file diff --git a/src/routes/health.ts b/src/routes/health.ts new file mode 100644 index 0000000..a1d8e19 --- /dev/null +++ b/src/routes/health.ts @@ -0,0 +1,14 @@ +import { FastifyInstance } from "fastify"; + +export default async function routes(server: FastifyInstance) { + server.get("/ping", async () => { + // Testquery gegen DB + const { data, error } = await server.supabase.from("tenants").select("id").limit(1); + + return { + status: "ok", + db: error ? "not connected" : "connected", + tenant_count: data?.length ?? 0 + }; + }); +} \ No newline at end of file diff --git a/src/routes/helpdesk.inbound.email.ts b/src/routes/helpdesk.inbound.email.ts new file mode 100644 index 0000000..5b24a4d --- /dev/null +++ b/src/routes/helpdesk.inbound.email.ts @@ -0,0 +1,104 @@ +// modules/helpdesk/helpdesk.inbound.email.ts +import { FastifyPluginAsync } from 'fastify' +import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js' +import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js' +import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' +import {extractDomain, findCustomerOrContactByEmailOrDomain} from "../utils/helpers"; +import {useNextNumberRangeNumber} from "../utils/functions"; + +// ------------------------------------------------------------- +// 📧 Interne M2M-Route für eingehende E-Mails +// ------------------------------------------------------------- + +const helpdeskInboundEmailRoutes: FastifyPluginAsync = async (server) => { + server.post('/helpdesk/inbound-email', async (req, res) => { + + const { + tenant_id, + channel_id, + from, + subject, + text, + message_id, + in_reply_to, + } = req.body as { + tenant_id: number + channel_id: string + from: {address: string, name: string} + subject: string + text: string + message_id: string + in_reply_to: string + } + + if (!tenant_id || !from?.address || !text) { + return res.status(400).send({ error: 'Invalid payload' }) + } + + server.log.info(`[InboundEmail] Neue Mail von ${from.address} für Tenant ${tenant_id}`) + + // 1️⃣ Kunde & Kontakt ermitteln + const { customer, contact: contactPerson } = + (await findCustomerOrContactByEmailOrDomain(server, from.address, tenant_id)) || {} + + // 2️⃣ Kontakt anlegen oder laden + const contact = await getOrCreateContact(server, tenant_id, { + email: from.address, + display_name: from.name || from.address, + customer_id: customer, + contact_id: contactPerson, + }) + + // 3️⃣ Konversation anhand In-Reply-To suchen + let conversationId: string | null = null + if (in_reply_to) { + const { data: msg } = await server.supabase + .from('helpdesk_messages') + .select('conversation_id') + .eq('external_message_id', in_reply_to) + .maybeSingle() + conversationId = msg?.conversation_id || null + } + + // 4️⃣ Neue Konversation anlegen falls keine existiert + let conversation + if (!conversationId) { + conversation = await createConversation(server, { + tenant_id, + contact, + channel_instance_id: channel_id, + subject: subject || '(kein Betreff)', + customer_id: customer, + contact_person_id: contactPerson, + }) + conversationId = conversation.id + } else { + const { data } = await server.supabase + .from('helpdesk_conversations') + .select('*') + .eq('id', conversationId) + .single() + conversation = data + } + + // 5️⃣ Nachricht speichern + await addMessage(server, { + tenant_id, + conversation_id: conversationId, + direction: 'incoming', + payload: { type: 'text', text }, + external_message_id: message_id, + raw_meta: { source: 'email' }, + }) + + server.log.info(`[InboundEmail] Ticket ${conversationId} gespeichert`) + + return res.status(201).send({ + success: true, + conversation_id: conversationId, + ticket_number: conversation.ticket_number, + }) + }) +} + +export default helpdeskInboundEmailRoutes diff --git a/src/routes/helpdesk.inbound.ts b/src/routes/helpdesk.inbound.ts new file mode 100644 index 0000000..49e8ec7 --- /dev/null +++ b/src/routes/helpdesk.inbound.ts @@ -0,0 +1,142 @@ +// modules/helpdesk/helpdesk.inbound.routes.ts +import { FastifyPluginAsync } from 'fastify' +import { createConversation } from '../modules/helpdesk/helpdesk.conversation.service.js' +import { addMessage } from '../modules/helpdesk/helpdesk.message.service.js' +import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' + +/** + * Öffentliche Route zum Empfang eingehender Kontaktformular-Nachrichten. + * Authentifizierung: über `public_token` aus helpdesk_channel_instances + */ + +function extractDomain(email) { + if (!email) return null + const parts = email.split("@") + return parts.length === 2 ? parts[1].toLowerCase() : null +} + +async function findCustomerOrContactByEmailOrDomain(server,fromMail, tenantId) { + const sender = fromMail + const senderDomain = extractDomain(sender) + if (!senderDomain) return null + + + // 1️⃣ Direkter Match über contacts + const { data: contactMatch } = await server.supabase + .from("contacts") + .select("id, customer") + .eq("email", sender) + .eq("tenant", tenantId) + .maybeSingle() + + if (contactMatch?.customer_id) return { + customer: contactMatch.customer, + contact: contactMatch.id + } + + // 2️⃣ Kunden laden, bei denen E-Mail oder Rechnungsmail passt + const { data: customers, error } = await server.supabase + .from("customers") + .select("id, infoData") + .eq("tenant", tenantId) + + if (error) { + console.error(`[Helpdesk] Fehler beim Laden der Kunden:`, error.message) + return null + } + + // 3️⃣ Durch Kunden iterieren und prüfen + for (const c of customers || []) { + const info = c.infoData || {} + const email = info.email?.toLowerCase() + const invoiceEmail = info.invoiceEmail?.toLowerCase() + + const emailDomain = extractDomain(email) + const invoiceDomain = extractDomain(invoiceEmail) + + // exakter Match oder Domain-Match + if ( + sender === email || + sender === invoiceEmail || + senderDomain === emailDomain || + senderDomain === invoiceDomain + ) { + return {customer: c.id, contact:null} + } + } + + return null +} + +const helpdeskInboundRoutes: FastifyPluginAsync = async (server) => { + // Öffentliche POST-Route + server.post('/helpdesk/inbound/:public_token', async (req, res) => { + const { public_token } = req.params as { public_token: string } + const { email, phone, display_name, subject, message } = req.body as { + email: string, + phone: string, + display_name: string + subject: string + message: string + } + + if (!message) { + return res.status(400).send({ error: 'Message content required' }) + } + + // 1️⃣ Kanalinstanz anhand des Tokens ermitteln + const { data: channel, error: channelError } = await server.supabase + .from('helpdesk_channel_instances') + .select('*') + .eq('public_token', public_token) + .single() + + if (channelError || !channel) { + return res.status(404).send({ error: 'Invalid channel token' }) + } + + const tenant_id = channel.tenant_id + const channel_instance_id = channel.id + + // @ts-ignore + const {customer, contact: contactPerson} = await findCustomerOrContactByEmailOrDomain(server,email, tenant_id ) + + + // 2️⃣ Kontakt finden oder anlegen + const contact = await getOrCreateContact(server, tenant_id, { + email, + phone, + display_name, + customer_id: customer, + contact_id: contactPerson, + }) + + // 3️⃣ Konversation erstellen + const conversation = await createConversation(server, { + tenant_id, + contact, + channel_instance_id, + subject: subject ?? 'Kontaktformular Anfrage', + customer_id: customer, + contact_person_id: contactPerson + }) + + // 4️⃣ Erste Nachricht hinzufügen + await addMessage(server, { + tenant_id, + conversation_id: conversation.id, + direction: 'incoming', + payload: { type: 'text', text: message }, + raw_meta: { source: 'contact_form' }, + }) + + // (optional) Auto-Antwort oder Event hier ergänzen + + return res.status(201).send({ + success: true, + conversation_id: conversation.id, + }) + }) +} + +export default helpdeskInboundRoutes diff --git a/src/routes/helpdesk.ts b/src/routes/helpdesk.ts new file mode 100644 index 0000000..4f9688e --- /dev/null +++ b/src/routes/helpdesk.ts @@ -0,0 +1,331 @@ +// modules/helpdesk/helpdesk.routes.ts +import { FastifyPluginAsync } from 'fastify' +import { createConversation, getConversations, updateConversationStatus } from '../modules/helpdesk/helpdesk.conversation.service.js' +import { addMessage, getMessages } from '../modules/helpdesk/helpdesk.message.service.js' +import { getOrCreateContact } from '../modules/helpdesk/helpdesk.contact.service.js' +import {decrypt, encrypt} from "../utils/crypt"; +import nodemailer from "nodemailer" + +const helpdeskRoutes: FastifyPluginAsync = async (server) => { + // 📩 1. Liste aller Konversationen + server.get('/helpdesk/conversations', async (req, res) => { + const tenant_id = req.user?.tenant_id + if (!tenant_id) return res.status(401).send({ error: 'Unauthorized' }) + + const { status } = req.query as {status: string} + const conversations = await getConversations(server, tenant_id, { status }) + return res.send(conversations) + }) + + // 🆕 2. Neue Konversation erstellen + server.post('/helpdesk/conversations', async (req, res) => { + const tenant_id = req.user?.tenant_id + if (!tenant_id) return res.status(401).send({ error: 'Unauthorized' }) + + const { contact, channel_instance_id, subject, message } = req.body as { + contact: object + channel_instance_id: string + subject: string + message: string + } + if (!contact || !channel_instance_id) { + return res.status(400).send({ error: 'Missing contact or channel_instance_id' }) + } + + // 1. Konversation erstellen + const conversation = await createConversation(server, { + tenant_id, + contact, + channel_instance_id, + subject, + }) + + // 2. Falls erste Nachricht vorhanden → hinzufügen + if (message) { + await addMessage(server, { + tenant_id, + conversation_id: conversation.id, + direction: 'incoming', + payload: { type: 'text', text: message }, + }) + } + + return res.status(201).send(conversation) + }) + + // 🧭 3. Einzelne Konversation abrufen + server.get('/helpdesk/conversations/:id', async (req, res) => { + const tenant_id = req.user?.tenant_id + const {id: conversation_id} = req.params as {id: string} + + const { data, error } = await server.supabase + .from('helpdesk_conversations') + .select('*, helpdesk_contacts(*)') + .eq('tenant_id', tenant_id) + .eq('id', conversation_id) + .single() + + if (error) return res.status(404).send({ error: 'Conversation not found' }) + return res.send(data) + }) + + // 🔄 4. Konversation Status ändern + server.patch('/helpdesk/conversations/:id/status', async (req, res) => { + const {id: conversation_id} = req.params as { id: string } + const { status } = req.body as { status: string } + + const updated = await updateConversationStatus(server, conversation_id, status) + return res.send(updated) + }) + + // 💬 5. Nachrichten abrufen + server.get('/helpdesk/conversations/:id/messages', async (req, res) => { + const {id:conversation_id} = req.params as { id: string } + const messages = await getMessages(server, conversation_id) + return res.send(messages) + }) + + // 💌 6. Nachricht hinzufügen (z. B. Antwort eines Agents) + server.post('/helpdesk/conversations/:id/messages', async (req, res) => { + console.log(req.user) + const tenant_id = req.user?.tenant_id + const author_user_id = req.user?.user_id + const {id: conversation_id} = req.params as { id: string } + const { text } = req.body as { text: string } + + if (!text) return res.status(400).send({ error: 'Missing message text' }) + + const message = await addMessage(server, { + tenant_id, + conversation_id, + author_user_id, + direction: 'outgoing', + payload: { type: 'text', text }, + }) + + return res.status(201).send(message) + }) + + // 👤 7. Kontakt suchen oder anlegen + server.post('/helpdesk/contacts', async (req, res) => { + const tenant_id = req.user?.tenant_id + const { email, phone, display_name } = req.body as { email: string; phone: string, display_name: string } + + const contact = await getOrCreateContact(server, tenant_id, { email, phone, display_name }) + return res.status(201).send(contact) + }) + + server.post("/helpdesk/channels", { + schema: { + body: { + type: "object", + required: ["type_id", "name", "config"], + properties: { + type_id: { type: "string" }, + name: { type: "string" }, + config: { type: "object" }, + is_active: { type: "boolean", default: true }, + }, + }, + }, + handler: async (req, reply) => { + const { type_id, name, config, is_active = true } = req.body as + { + type_id: string, + name: string, + config: { + imap:{ + host: string | object, + user: string | object, + pass: string | object, + }, + smtp:{ + host: string | object, + user: string | object, + pass: string | object, + } + }, + is_active: boolean + } + + // 🔒 Tenant aus Auth-Context + const tenant_id = req.user?.tenant_id + if (!tenant_id) { + return reply.status(401).send({ error: "Kein Tenant im Benutzerkontext gefunden." }) + } + + if (type_id !== "email") { + return reply.status(400).send({ error: "Nur Typ 'email' wird aktuell unterstützt." }) + } + + try { + const safeConfig = { ...config } + + // 🔐 IMAP-Daten verschlüsseln + if (safeConfig.imap) { + if (safeConfig.imap.host) + safeConfig.imap.host = encrypt(safeConfig.imap.host) + if (safeConfig.imap.user) + safeConfig.imap.user = encrypt(safeConfig.imap.user) + if (safeConfig.imap.pass) + safeConfig.imap.pass = encrypt(safeConfig.imap.pass) + } + + // 🔐 SMTP-Daten verschlüsseln + if (safeConfig.smtp) { + if (safeConfig.smtp.host) + safeConfig.smtp.host = encrypt(safeConfig.smtp.host) + if (safeConfig.smtp.user) + safeConfig.smtp.user = encrypt(safeConfig.smtp.user) + if (safeConfig.smtp.pass) + safeConfig.smtp.pass = encrypt(safeConfig.smtp.pass) + } + + // Speichern in Supabase + const { data, error } = await server.supabase + .from("helpdesk_channel_instances") + .insert({ + tenant_id, + type_id, + name, + config: safeConfig, + is_active, + }) + .select() + .single() + + if (error) throw error + + // sensible Felder aus Response entfernen + if (data.config?.imap) { + delete data.config.imap.host + delete data.config.imap.user + delete data.config.imap.pass + } + if (data.config?.smtp) { + delete data.config.smtp.host + delete data.config.smtp.user + delete data.config.smtp.pass + } + + reply.send({ + message: "E-Mail-Channel erfolgreich erstellt", + channel: data, + }) + } catch (err) { + console.error("Fehler bei Channel-Erstellung:", err) + reply.status(500).send({ error: err.message }) + } + }, + }) + + server.post("/helpdesk/conversations/:id/reply", { + schema: { + body: { + type: "object", + required: ["text"], + properties: { + text: { type: "string" }, + }, + }, + }, + handler: async (req, reply) => { + const conversationId = (req.params as any).id + const { text } = req.body as { text: string } + + // 🔹 Konversation inkl. Channel + Kontakt laden + const { data: conv, error: convErr } = await server.supabase + .from("helpdesk_conversations") + .select(` + id, + tenant_id, + subject, + channel_instance_id, + helpdesk_contacts(email), + helpdesk_channel_instances(config, name), + ticket_number + `) + .eq("id", conversationId) + .single() + + console.log(conv) + + if (convErr || !conv) { + reply.status(404).send({ error: "Konversation nicht gefunden" }) + return + } + + const contact = conv.helpdesk_contacts as unknown as {email: string} + const channel = conv.helpdesk_channel_instances as unknown as {name: string} + + console.log(contact) + if (!contact?.email) { + reply.status(400).send({ error: "Kein Empfänger gefunden" }) + return + } + + // 🔐 SMTP-Daten entschlüsseln + try { + // @ts-ignore + const smtp = channel?.config?.smtp + const host = + typeof smtp.host === "object" ? decrypt(smtp.host) : smtp.host + const user = + typeof smtp.user === "object" ? decrypt(smtp.user) : smtp.user + const pass = + typeof smtp.pass === "object" ? decrypt(smtp.pass) : smtp.pass + + // 🔧 Transporter + const transporter = nodemailer.createTransport({ + host, + port: smtp.port || 465, + secure: smtp.secure ?? true, + auth: { user, pass }, + }) + + // 📩 Mail senden + + const mailOptions = { + from: `"${channel?.name}" <${user}>`, + to: contact.email, + subject: `${conv.ticket_number} | ${conv.subject}` || `${conv.ticket_number} | Antwort vom FEDEO Helpdesk`, + text, + } + + const info = await transporter.sendMail(mailOptions) + console.log(`[Helpdesk SMTP] Gesendet an ${contact.email}: ${info.messageId}`) + + // 💾 Nachricht speichern + const { error: insertErr } = await server.supabase + .from("helpdesk_messages") + .insert({ + tenant_id: conv.tenant_id, + conversation_id: conversationId, + direction: "outgoing", + payload: { type: "text", text }, + external_message_id: info.messageId, + received_at: new Date().toISOString(), + }) + + if (insertErr) throw insertErr + + // 🔁 Konversation aktualisieren + await server.supabase + .from("helpdesk_conversations") + .update({ last_message_at: new Date().toISOString() }) + .eq("id", conversationId) + + reply.send({ + message: "E-Mail erfolgreich gesendet", + messageId: info.messageId, + }) + } catch (err: any) { + console.error("Fehler beim SMTP-Versand:", err) + reply.status(500).send({ error: err.message }) + } + }, + }) + +} + +export default helpdeskRoutes diff --git a/src/routes/history.ts b/src/routes/history.ts new file mode 100644 index 0000000..dca8105 --- /dev/null +++ b/src/routes/history.ts @@ -0,0 +1,156 @@ +// src/routes/resources/history.ts +import { FastifyInstance } from "fastify"; + +const columnMap: Record = { + customers: "customer", + vendors: "vendor", + projects: "project", + plants: "plant", + contracts: "contract", + contacts: "contact", + tasks: "task", + vehicles: "vehicle", + events: "event", + files: "file", + products: "product", + inventoryitems: "inventoryitem", + inventoryitemgroups: "inventoryitemgroup", + absencerequests: "absencerequest", + checks: "check", + costcentres: "costcentre", + ownaccounts: "ownaccount", + documentboxes: "documentbox", + hourrates: "hourrate", + services: "service", + roles: "role", +}; + +export default async function resourceHistoryRoutes(server: FastifyInstance) { + server.get<{ + Params: { resource: string; id: string } + }>("/resource/:resource/:id/history", { + schema: { + tags: ["History"], + summary: "Get history entries for a resource", + params: { + type: "object", + required: ["resource", "id"], + properties: { + resource: { type: "string" }, + id: { type: "string" }, + }, + }, + }, + }, async (req, reply) => { + const { resource, id } = req.params; + + const column = columnMap[resource]; + if (!column) { + return reply.code(400).send({ error: `History not supported for resource '${resource}'` }); + } + + const { data, error } = await server.supabase + .from("historyitems") + .select("*") + .eq(column, id) + .order("created_at", { ascending: true }); + + if (error) { + server.log.error(error); + return reply.code(500).send({ error: "Failed to fetch history" }); + } + + const {data:users, error:usersError} = await server.supabase + .from("auth_users") + .select("*, auth_profiles(*), tenants!auth_tenant_users(*)") + + const filteredUsers = (users ||[]).filter(i => i.tenants.find((t:any) => t.id === req.user?.tenant_id)) + + const dataCombined = data.map(historyitem => { + return { + ...historyitem, + created_by_profile: filteredUsers.find(i => i.id === historyitem.created_by) ? filteredUsers.find(i => i.id === historyitem.created_by).auth_profiles[0] : null + } + }) + + + + return dataCombined; + }); + + // Neuen HistoryItem anlegen + server.post<{ + Params: { resource: string; id: string }; + Body: { + text: string; + old_val?: string | null; + new_val?: string | null; + config?: Record; + }; + }>("/resource/:resource/:id/history", { + schema: { + tags: ["History"], + summary: "Create new history entry", + params: { + type: "object", + properties: { + resource: { type: "string" }, + id: { type: "string" } + }, + required: ["resource", "id"] + }, + body: { + type: "object", + properties: { + text: { type: "string" }, + old_val: { type: "string", nullable: true }, + new_val: { type: "string", nullable: true }, + config: { type: "object", nullable: true } + }, + required: ["text"] + }, + response: { + 201: { + type: "object", + properties: { + id: { type: "number" }, + text: { type: "string" }, + created_at: { type: "string" }, + created_by: { type: "string" } + } + } + } + } + }, async (req, reply) => { + const { resource, id } = req.params; + const { text, old_val, new_val, config } = req.body; + + const userId = (req.user as any)?.user_id; + + + const fkField = columnMap[resource]; + if (!fkField) { + return reply.code(400).send({ error: `Unknown resource: ${resource}` }); + } + + const { data, error } = await server.supabase + .from("historyitems") + .insert({ + text, + [fkField]: id, + oldVal: old_val || null, + newVal: new_val || null, + config: config || null, + tenant: (req.user as any)?.tenant_id, + created_by: userId + }) + .select() + .single(); + + if (error) { + return reply.code(500).send({ error: error.message }); + } + + return reply.code(201).send(data); + }); +} diff --git a/src/routes/internal/devices.ts b/src/routes/internal/devices.ts new file mode 100644 index 0000000..640304b --- /dev/null +++ b/src/routes/internal/devices.ts @@ -0,0 +1,41 @@ +import { FastifyInstance } from "fastify"; +import { eq } from "drizzle-orm"; +import { devices } from "../../../db/schema"; + +export default async function deviceRoutes(fastify: FastifyInstance) { + fastify.get<{ + Params: { + externalId: string; + }; + }>( + "/devices/by-external-id/:externalId", + async (request, reply) => { + const { externalId } = request.params; + + const device = await fastify.db + .select({ + id: devices.id, + name: devices.name, + type: devices.type, + tenant: devices.tenant, + externalId: devices.externalId, + created_at: devices.createdAt, + }) + .from(devices) + .where( + eq(devices.externalId, externalId) + + ) + .limit(1) + .then(rows => rows[0]); + + if (!device) { + return reply.status(404).send({ + message: "Device not found", + }); + } + + return reply.send(device); + } + ); +} diff --git a/src/routes/internal/tenant.ts b/src/routes/internal/tenant.ts new file mode 100644 index 0000000..ace0dc4 --- /dev/null +++ b/src/routes/internal/tenant.ts @@ -0,0 +1,107 @@ +import { FastifyInstance } from "fastify" + +import { + authTenantUsers, + authUsers, + authProfiles, + tenants +} from "../../../db/schema" + +import {and, eq, inArray} from "drizzle-orm" + + +export default async function tenantRoutesInternal(server: FastifyInstance) { + + + // ------------------------------------------------------------- + // GET CURRENT TENANT + // ------------------------------------------------------------- + server.get("/tenant/:id", async (req) => { + //@ts-ignore + const tenant = (await server.db.select().from(tenants).where(eq(tenants.id,req.params.id)).limit(1))[0] + + return tenant + }) + + + // ------------------------------------------------------------- + // TENANT USERS (auth_users + auth_profiles) + // ------------------------------------------------------------- + server.get("/tenant/users", async (req, reply) => { + try { + const authUser = req.user + if (!authUser) return reply.code(401).send({ error: "Unauthorized" }) + + const tenantId = authUser.tenant_id + + // 1) auth_tenant_users → user_ids + const tenantUsers = await server.db + .select() + .from(authTenantUsers) + .where(eq(authTenantUsers.tenant_id, tenantId)) + + const userIds = tenantUsers.map(u => u.user_id) + + if (!userIds.length) { + return { tenant_id: tenantId, users: [] } + } + + // 2) auth_users laden + const users = await server.db + .select() + .from(authUsers) + .where(inArray(authUsers.id, userIds)) + + // 3) auth_profiles pro Tenant laden + const profiles = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.tenant_id, tenantId), + inArray(authProfiles.user_id, userIds) + )) + + const combined = users.map(u => { + const profile = profiles.find(p => p.user_id === u.id) + return { + id: u.id, + email: u.email, + profile, + full_name: profile?.full_name ?? null + } + }) + + return { tenant_id: tenantId, users: combined } + + } catch (err) { + console.error("/tenant/users ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // TENANT PROFILES + // ------------------------------------------------------------- + server.get("/tenant/:id/profiles", async (req, reply) => { + try { + // @ts-ignore + const tenantId = req.params.id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const data = await server.db + .select() + .from(authProfiles) + .where(eq(authProfiles.tenant_id, tenantId)) + + return data + + } catch (err) { + console.error("/tenant/profiles ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + +} diff --git a/src/routes/internal/time.ts b/src/routes/internal/time.ts new file mode 100644 index 0000000..89b0544 --- /dev/null +++ b/src/routes/internal/time.ts @@ -0,0 +1,122 @@ +import { FastifyInstance } from "fastify" +import { + stafftimeentries, + stafftimenetryconnects +} from "../../../db/schema" +import { + eq, + and, + gte, + lte, + desc +} from "drizzle-orm" +import {stafftimeevents} from "../../../db/schema/staff_time_events"; +import {loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service"; +import {deriveTimeSpans} from "../../modules/time/derivetimespans.service"; +import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service"; +import {z} from "zod"; +import {enrichSpansWithStatus} from "../../modules/time/enrichtimespanswithstatus.service"; + +export default async function staffTimeRoutesInternal(server: FastifyInstance) { + + + server.post("/staff/time/event", async (req, reply) => { + try { + + const body = req.body as {user_id:string,tenant_id:number,eventtime:string,eventtype:string} + + const normalizeDate = (val: any) => { + if (!val) return null + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + const dataToInsert = { + tenant_id: body.tenant_id, + user_id: body.user_id, + actortype: "user", + actoruser_id: body.user_id, + eventtime: normalizeDate(body.eventtime), + eventtype: body.eventtype, + source: "WEB" + } + + console.log(dataToInsert) + + const [created] = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(dataToInsert) + .returning() + + return created + } catch (err: any) { + console.error(err) + return reply.code(400).send({ error: err.message }) + } + }) + + + // GET /api/staff/time/spans + server.get("/staff/time/spans", async (req, reply) => { + try { + + // Query-Parameter: targetUserId ist optional + const { targetUserId, tenantId} = req.query as { targetUserId: string, tenantId:number }; + + // Falls eine targetUserId übergeben wurde, nutzen wir diese, sonst die eigene ID + const evaluatedUserId = targetUserId; + + // 💡 "Unendlicher" Zeitraum, wie gewünscht + const startDate = new Date(0); // 1970 + const endDate = new Date("2100-12-31"); + + // SCHRITT 1: Lade ALLE Events für den ZIEL-USER (evaluatedUserId) + const allEventsInTimeFrame = await loadValidEvents( + server, + tenantId, + evaluatedUserId, // <--- Hier wird jetzt die variable ID genutzt + startDate, + endDate + ); + + // SCHRITT 2: Filtere faktische Events + const FACTUAL_EVENT_TYPES = new Set([ + "work_start", "work_end", "pause_start", "pause_end", + "sick_start", "sick_end", "vacation_start", "vacation_end", + "overtime_compensation_start", "overtime_compensation_end", + "auto_stop" + ]); + const factualEvents = allEventsInTimeFrame.filter(e => FACTUAL_EVENT_TYPES.has(e.eventtype)); + + // SCHRITT 3: Hole administrative Events + const factualEventIds = factualEvents.map(e => e.id); + + if (factualEventIds.length === 0) { + return []; + } + + const relatedAdminEvents = await loadRelatedAdminEvents(server, factualEventIds); + + // SCHRITT 4: Kombinieren und Sortieren + const combinedEvents = [ + ...factualEvents, + ...relatedAdminEvents, + ].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime()); + + // SCHRITT 5: Spans ableiten + const derivedSpans = deriveTimeSpans(combinedEvents); + + // SCHRITT 6: Spans anreichern + const enrichedSpans = enrichSpansWithStatus(derivedSpans, combinedEvents); + + return enrichedSpans; + + } catch (error) { + console.error("Fehler beim Laden der Spans:", error); + return reply.code(500).send({ error: "Interner Fehler beim Laden der Zeitspannen." }); + } + }); + + +} diff --git a/src/routes/notifications.ts b/src/routes/notifications.ts new file mode 100644 index 0000000..395a6c5 --- /dev/null +++ b/src/routes/notifications.ts @@ -0,0 +1,30 @@ +// routes/notifications.routes.ts +import { FastifyInstance } from 'fastify'; +import { NotificationService, UserDirectory } from '../modules/notification.service'; + +// Beispiel: E-Mail aus eigener User-Tabelle laden +const getUserDirectory: UserDirectory = async (server:FastifyInstance, userId, tenantId) => { + const { data, error } = await server.supabase + .from('auth_users') + .select('email') + .eq('id', userId) + .maybeSingle(); + if (error || !data) return null; + return { email: data.email }; +}; + +export default async function notificationsRoutes(server: FastifyInstance) { + // wichtig: server.supabase ist über app verfügbar + + const svc = new NotificationService(server, getUserDirectory); + + server.post('/notifications/trigger', async (req, reply) => { + try { + const res = await svc.trigger(req.body as any); + reply.send(res); + } catch (err: any) { + server.log.error(err); + reply.code(500).send({ error: err.message }); + } + }); +} diff --git a/src/routes/profiles.ts b/src/routes/profiles.ts new file mode 100644 index 0000000..833f748 --- /dev/null +++ b/src/routes/profiles.ts @@ -0,0 +1,120 @@ +import { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; + +import { + authProfiles, +} from "../../db/schema"; + +export default async function authProfilesRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // GET SINGLE PROFILE + // ------------------------------------------------------------- + server.get("/profiles/:id", async (req, reply) => { + try { + const { id } = req.params as { id: string }; + const tenantId = (req.user as any)?.tenant_id; + + if (!tenantId) { + return reply.code(400).send({ error: "No tenant selected" }); + } + + const rows = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.id, id), + eq(authProfiles.tenant_id, tenantId) + ) + ) + .limit(1); + + if (!rows.length) { + return reply.code(404).send({ error: "User not found or not in tenant" }); + } + + return rows[0]; + + } catch (error) { + console.error("GET /profiles/:id ERROR:", error); + return reply.code(500).send({ error: "Internal Server Error" }); + } + }); + + function sanitizeProfileUpdate(body: any) { + const cleaned: any = { ...body } + + // ❌ Systemfelder entfernen + const forbidden = [ + "id", "user_id", "tenant_id", "created_at", "updated_at", + "updatedAt", "updatedBy", "old_profile_id", "full_name" + ] + forbidden.forEach(f => delete cleaned[f]) + + // ❌ Falls NULL Strings vorkommen → in null umwandeln + for (const key of Object.keys(cleaned)) { + if (cleaned[key] === "") cleaned[key] = null + } + + // ✅ Date-Felder sauber konvertieren, falls vorhanden + const dateFields = ["birthday", "entry_date"] + + for (const field of dateFields) { + if (cleaned[field]) { + const d = new Date(cleaned[field]) + if (!isNaN(d.getTime())) cleaned[field] = d + else delete cleaned[field] // invalid → entfernen + } + } + + return cleaned + } + + // ------------------------------------------------------------- + // UPDATE PROFILE + // ------------------------------------------------------------- + server.put("/profiles/:id", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id + const userId = req.user?.user_id + + if (!tenantId) { + return reply.code(400).send({ error: "No tenant selected" }) + } + + const { id } = req.params as { id: string } + let body = req.body as any + + // Clean + Normalize + body = sanitizeProfileUpdate(body) + + const updateData = { + ...body, + updatedAt: new Date(), + updatedBy: userId + } + + const updated = await server.db + .update(authProfiles) + .set(updateData) + .where( + and( + eq(authProfiles.id, id), + eq(authProfiles.tenant_id, tenantId) + ) + ) + .returning() + + if (!updated.length) { + return reply.code(404).send({ error: "User not found or not in tenant" }) + } + + return updated[0] + + } catch (err) { + console.error("PUT /profiles/:id ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) +} diff --git a/src/routes/publiclinks/publiclinks-authenticated.ts b/src/routes/publiclinks/publiclinks-authenticated.ts new file mode 100644 index 0000000..33e8fa5 --- /dev/null +++ b/src/routes/publiclinks/publiclinks-authenticated.ts @@ -0,0 +1,41 @@ +import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'; +import { publicLinkService } from '../../modules/publiclinks.service'; + + +export default async function publiclinksAuthenticatedRoutes(server: FastifyInstance) { + server.post("/publiclinks", async (req, reply) => { + try { + const tenantId = 21; // Hardcoded für Test, später: req.user.tenantId + + const { name, isProtected, pin, customToken, config, defaultProfileId } = req.body as { name:string, isProtected:boolean, pin:string, customToken:string, config:Object, defaultProfileId:string}; + + const newLink = await publicLinkService.createLink(server, tenantId, + name, + isProtected, + pin, + customToken, + config, + defaultProfileId); + + return reply.code(201).send({ + success: true, + data: { + id: newLink.id, + token: newLink.token, + fullUrl: `/public/${newLink.token}`, // Helper für Frontend + isProtected: newLink.isProtected + } + }); + + } catch (error: any) { + server.log.error(error); + + // Einfache Fehlerbehandlung + if (error.message.includes("bereits vergeben")) { + return reply.code(409).send({ error: error.message }); + } + + return reply.code(500).send({ error: "Fehler beim Erstellen des Links", details: error.message }); + } + }) +} diff --git a/src/routes/publiclinks/publiclinks-non-authenticated.ts b/src/routes/publiclinks/publiclinks-non-authenticated.ts new file mode 100644 index 0000000..ded3b64 --- /dev/null +++ b/src/routes/publiclinks/publiclinks-non-authenticated.ts @@ -0,0 +1,91 @@ +import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'; +import { publicLinkService } from '../../modules/publiclinks.service'; + + +export default async function publiclinksNonAuthenticatedRoutes(server: FastifyInstance) { + server.get("/workflows/context/:token", async (req, reply) => { + const { token } = req.params as { token: string }; + + // Wir lesen die PIN aus dem Header (Best Practice für Security) + const pin = req.headers['x-public-pin'] as string | undefined; + + try { + const context = await publicLinkService.getLinkContext(server, token, pin); + + return reply.send(context); + + } catch (error: any) { + // Spezifische Fehlercodes für das Frontend + if (error.message === "Link_NotFound") { + return reply.code(404).send({ error: "Link nicht gefunden oder abgelaufen" }); + } + + if (error.message === "Pin_Required") { + return reply.code(401).send({ + error: "PIN erforderlich", + code: "PIN_REQUIRED", + requirePin: true + }); + } + + if (error.message === "Pin_Invalid") { + return reply.code(403).send({ + error: "PIN falsch", + code: "PIN_INVALID", + requirePin: true + }); + } + + server.log.error(error); + return reply.code(500).send({ error: "Interner Server Fehler" }); + } + }); + + server.post("/workflows/submit/:token", async (req, reply) => { + const { token } = req.params as { token: string }; + // PIN sicher aus dem Header lesen + const pin = req.headers['x-public-pin'] as string | undefined; + // Der Body enthält { profile, project, service, ... } + const payload = req.body; + + console.log(payload) + + try { + // Service aufrufen (führt die 3 Schritte aus: Lieferschein -> Zeit -> History) + const result = await publicLinkService.submitFormData(server, token, payload, pin); + + // 201 Created zurückgeben + return reply.code(201).send(result); + + } catch (error: any) { + console.log(error); + + // Fehler-Mapping für saubere HTTP Codes + if (error.message === "Link_NotFound") { + return reply.code(404).send({ error: "Link ungültig oder nicht aktiv" }); + } + + if (error.message === "Pin_Required") { + return reply.code(401).send({ error: "PIN erforderlich" }); + } + + if (error.message === "Pin_Invalid") { + return reply.code(403).send({ error: "PIN ist falsch" }); + } + + if (error.message === "Profile_Missing") { + return reply.code(400).send({ error: "Kein Mitarbeiter-Profil gefunden (weder im Link noch in der Eingabe)" }); + } + + if (error.message === "Project not found" || error.message === "Service not found") { + return reply.code(400).send({ error: "Ausgewähltes Projekt oder Leistung existiert nicht mehr." }); + } + + // Fallback für alle anderen Fehler (z.B. DB Constraints) + return reply.code(500).send({ + error: "Interner Fehler beim Speichern", + details: error.message + }); + } + }); +} diff --git a/src/routes/resources/main.ts b/src/routes/resources/main.ts new file mode 100644 index 0000000..1c65d3d --- /dev/null +++ b/src/routes/resources/main.ts @@ -0,0 +1,555 @@ +import { FastifyInstance } from "fastify" +import { + eq, + ilike, + asc, + desc, + and, + count, + inArray, + or +} from "drizzle-orm" + + + +import {resourceConfig} from "../../utils/resource.config"; +import {useNextNumberRangeNumber} from "../../utils/functions"; +import {stafftimeentries} from "../../../db/schema"; + +// ------------------------------------------------------------- +// SQL Volltextsuche auf mehreren Feldern +// ------------------------------------------------------------- + + +function buildSearchCondition(table: any, columns: string[], search: string) { + if (!search || !columns.length) return null + + const term = `%${search.toLowerCase()}%` + + const conditions = columns + .map((colName) => table[colName]) + .filter(Boolean) + .map((col) => ilike(col, term)) + + if (conditions.length === 0) return null + + // @ts-ignore + return or(...conditions) +} + +export default async function resourceRoutes(server: FastifyInstance) { + + // ------------------------------------------------------------- + // LIST + // ------------------------------------------------------------- + server.get("/resource/:resource", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id + if (!tenantId) + return reply.code(400).send({ error: "No tenant selected" }) + + const { search, sort, asc: ascQuery } = req.query as { + search?: string + sort?: string + asc?: string + } + + const {resource} = req.params as {resource: string} + const table = resourceConfig[resource].table + + // WHERE-Basis + let whereCond: any = eq(table.tenant, tenantId) + + // 🔍 SQL Search + if(search) { + const searchCond = buildSearchCondition( + table, + resourceConfig[resource].searchColumns, + search.trim() + ) + + if (searchCond) { + whereCond = and(whereCond, searchCond) + } + } + + // Base Query + let q = server.db.select().from(table).where(whereCond) + + // Sortierung + if (sort) { + const col = (table as any)[sort] + if (col) { + //@ts-ignore + q = ascQuery === "true" + ? q.orderBy(asc(col)) + : q.orderBy(desc(col)) + } + } + + const queryData = await q + + // RELATION LOADING (MANY-TO-ONE) + + let ids = {} + let lists = {} + let maps = {} + let data = [...queryData] + + if(resourceConfig[resource].mtoLoad) { + resourceConfig[resource].mtoLoad.forEach(relation => { + ids[relation] = [...new Set(queryData.map(r => r[relation]).filter(Boolean))]; + }) + + for await (const relation of resourceConfig[resource].mtoLoad ) { + console.log(relation) + lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] + + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); + }) + + data = queryData.map(row => { + let toReturn = { + ...row + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null + }) + + return toReturn + }); + } + + if(resourceConfig[resource].mtmListLoad) { + for await (const relation of resourceConfig[resource].mtmListLoad) { + console.log(relation) + console.log(resource.substring(0,resource.length-1)) + + const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id))) + + console.log(relationRows.length) + + data = data.map(row => { + let toReturn = { + ...row + } + + toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id) + + return toReturn + }) + + + } + } + + return data + + } catch (err) { + console.error("ERROR /resource/:resource", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + // ------------------------------------------------------------- + // PAGINATED LIST + // ------------------------------------------------------------- + server.get("/resource/:resource/paginated", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id; + if (!tenantId) { + return reply.code(400).send({ error: "No tenant selected" }); + } + + const {resource} = req.params as {resource: string}; + + const {queryConfig} = req; + const { + pagination, + sort, + filters, + paginationDisabled + } = queryConfig; + + const { search, distinctColumns } = req.query as { + search?: string; + distinctColumns?: string; + }; + + + + + let table = resourceConfig[resource].table + + + let whereCond: any = eq(table.tenant, tenantId); + + + if(search) { + const searchCond = buildSearchCondition( + table, + resourceConfig[resource].searchColumns, + search.trim() + ) + + if (searchCond) { + whereCond = and(whereCond, searchCond) + } + } + + if (filters) { + for (const [key, val] of Object.entries(filters)) { + const col = (table as any)[key]; + if (!col) continue; + + if (Array.isArray(val)) { + whereCond = and(whereCond, inArray(col, val)); + } else { + whereCond = and(whereCond, eq(col, val as any)); + } + } + } + + // ----------------------------------------------- + // COUNT (for pagination) + // ----------------------------------------------- + const totalRes = await server.db + .select({ value: count(table.id) }) + .from(table) + .where(whereCond); + + const total = Number(totalRes[0]?.value ?? 0); + + // ----------------------------------------------- + // DISTINCT VALUES (regardless of pagination) + // ----------------------------------------------- + const distinctValues: Record = {}; + + if (distinctColumns) { + for (const colName of distinctColumns.split(",").map(c => c.trim())) { + const col = (table as any)[colName]; + if (!col) continue; + + const rows = await server.db + .select({ v: col }) + .from(table) + .where(eq(table.tenant, tenantId)); + + const values = rows + .map(r => r.v) + .filter(v => v != null && v !== ""); + + distinctValues[colName] = [...new Set(values)].sort(); + } + } + + // PAGINATION + const offset = pagination?.offset ?? 0; + const limit = pagination?.limit ?? 100; + + // SORTING + let orderField: any = null; + let direction: "asc" | "desc" = "asc"; + + if (sort?.length > 0) { + const s = sort[0]; + const col = (table as any)[s.field]; + if (col) { + orderField = col; + direction = s.direction === "asc" ? "asc" : "desc"; + } + } + + // MAIN QUERY (Paginated) + let q = server.db + .select() + .from(table) + .where(whereCond) + .offset(offset) + .limit(limit); + + if (orderField) { + //@ts-ignore + q = direction === "asc" + ? q.orderBy(asc(orderField)) + : q.orderBy(desc(orderField)); + } + + const rows = await q; + + if (!rows.length) { + return { + data: [], + queryConfig: { + ...queryConfig, + total, + totalPages: 0, + distinctValues + } + }; + } + + + + let data = [...rows] + //Many to One + if(resourceConfig[resource].mtoLoad) { + let ids = {} + let lists = {} + let maps = {} + resourceConfig[resource].mtoLoad.forEach(relation => { + ids[relation] = [...new Set(rows.map(r => r[relation]).filter(Boolean))]; + }) + + for await (const relation of resourceConfig[resource].mtoLoad ) { + lists[relation] = ids[relation].length ? await server.db.select().from(resourceConfig[relation + "s"].table).where(inArray(resourceConfig[relation + "s"].table.id, ids[relation])) : [] + + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + maps[relation] = Object.fromEntries(lists[relation].map(i => [i.id, i])); + }) + + data = rows.map(row => { + let toReturn = { + ...row + } + + resourceConfig[resource].mtoLoad.forEach(relation => { + toReturn[relation] = row[relation] ? maps[relation][row[relation]] : null + }) + + return toReturn + }); + } + + if(resourceConfig[resource].mtmListLoad) { + for await (const relation of resourceConfig[resource].mtmListLoad) { + console.log(relation) + + const relationRows = await server.db.select().from(resourceConfig[relation].table).where(inArray(resourceConfig[relation].table[resource.substring(0,resource.length-1)],data.map(i => i.id))) + + console.log(relationRows) + + data = data.map(row => { + let toReturn = { + ...row + } + + toReturn[relation] = relationRows.filter(i => i[resource.substring(0,resource.length-1)] === row.id) + + return toReturn + }) + + + } + } + + // ----------------------------------------------- + // RETURN DATA + // ----------------------------------------------- + return { + data, + queryConfig: { + ...queryConfig, + total, + totalPages: Math.ceil(total / limit), + distinctValues + } + }; + + } catch (err) { + console.error(`ERROR /resource/:resource/paginated:`, err); + return reply.code(500).send({ error: "Internal Server Error" }); + } + }); + + + // ------------------------------------------------------------- + // DETAIL (mit JOINS) + // ------------------------------------------------------------- + server.get("/resource/:resource/:id/:no_relations?", async (req, reply) => { + try { + const { id } = req.params as { id: string } + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(400).send({ error: "No tenant selected" }) + + const {resource, no_relations} = req.params as { resource: string, no_relations?: boolean } + const table = resourceConfig[resource].table + + const projRows = await server.db + .select() + .from(table) + .where(and(eq(table.id, id), eq(table.tenant, tenantId))) + .limit(1) + + if (!projRows.length) + return reply.code(404).send({ error: "Resource not found" }) + + // ------------------------------------ + // LOAD RELATIONS + // ------------------------------------ + + let ids = {} + let lists = {} + let maps = {} + let data = { + ...projRows[0] + } + + if(!no_relations) { + if(resourceConfig[resource].mtoLoad) { + for await (const relation of resourceConfig[resource].mtoLoad ) { + if(data[relation]) { + data[relation] = (await server.db.select().from(resourceConfig[relation + "s"].table).where(eq(resourceConfig[relation + "s"].table.id, data[relation])))[0] + } + } + } + + if(resourceConfig[resource].mtmLoad) { + for await (const relation of resourceConfig[resource].mtmLoad ) { + console.log(relation) + data[relation] = await server.db.select().from(resourceConfig[relation].table).where(eq(resourceConfig[relation].table[resource.substring(0,resource.length - 1)],id)) + } + } + } + + + + return data + + } catch (err) { + console.error("ERROR /resource/projects/:id", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + // Create + server.post("/resource/:resource", async (req, reply) => { + try { + if (!req.user?.tenant_id) { + return reply.code(400).send({error: "No tenant selected"}); + } + + const {resource} = req.params as { resource: string }; + const body = req.body as Record; + + const table = resourceConfig[resource].table + + let createData = { + ...body, + tenant: req.user.tenant_id, + archived: false, // Standardwert + } + + console.log(resourceConfig[resource].numberRangeHolder) + + if (resourceConfig[resource].numberRangeHolder && !body[resourceConfig[resource]]) { + const result = await useNextNumberRangeNumber(server, req.user.tenant_id, resource) + console.log(result) + createData[resourceConfig[resource].numberRangeHolder] = result.usedNumber + } + + const normalizeDate = (val: any) => { + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + Object.keys(createData).forEach((key) => { + if(key.toLowerCase().includes("date")) createData[key] = normalizeDate(createData[key]) + }) + + + const [created] = await server.db + .insert(table) + .values(createData) + .returning() + + + /*await insertHistoryItem(server, { + entity: resource, + entityId: data.id, + action: "created", + created_by: req.user.user_id, + tenant_id: req.user.tenant_id, + oldVal: null, + newVal: data, + text: `${dataType.labelSingle} erstellt`, + });*/ + + return created; + } catch (error) { + console.log(error) + reply.status(500) + } + }); + + // UPDATE (inkl. Soft-Delete/Archive) + server.put("/resource/:resource/:id", async (req, reply) => { + try { + const {resource, id} = req.params as { resource: string; id: string } + const body = req.body as Record + + const tenantId = (req.user as any)?.tenant_id + const userId = (req.user as any)?.user_id + + if (!tenantId || !userId) { + return reply.code(401).send({error: "Unauthorized"}) + } + + const table = resourceConfig[resource].table + + //TODO: HISTORY + + const normalizeDate = (val: any) => { + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + let data = {...body, updated_at: new Date().toISOString(), updated_by: userId} + + Object.keys(data).forEach((key) => { + if(key.includes("_at") || key.includes("At")) { + data[key] = normalizeDate(data[key]) + } + }) + + console.log(data) + + const [updated] = await server.db + .update(table) + .set(data) + .where(and( + eq(table.id, id), + eq(table.tenant, tenantId))) + .returning() + + //const diffs = diffObjects(oldItem, newItem); + + + /*for (const d of diffs) { + await insertHistoryItem(server, { + entity: resource, + entityId: id, + action: d.type, + created_by: userId, + tenant_id: tenantId, + oldVal: d.oldValue ? String(d.oldValue) : null, + newVal: d.newValue ? String(d.newValue) : null, + text: `Feld "${d.label}" ${d.typeLabel}: ${d.oldValue ?? ""} → ${d.newValue ?? ""}`, + }); + }*/ + + return updated + } catch (err) { + console.log("ERROR /resource/projects/:id", err) + } + + }) + +} diff --git a/src/routes/resourcesSpecial.ts b/src/routes/resourcesSpecial.ts new file mode 100644 index 0000000..fc29890 --- /dev/null +++ b/src/routes/resourcesSpecial.ts @@ -0,0 +1,75 @@ +import { FastifyInstance } from "fastify" +import { asc, desc } from "drizzle-orm" +import { sortData } from "../utils/sort" + +// Schema imports +import { accounts, units,countrys } from "../../db/schema" + +const TABLE_MAP: Record = { + accounts, + units, + countrys, +} + +export default async function resourceRoutesSpecial(server: FastifyInstance) { + + server.get("/resource-special/:resource", async (req, reply) => { + try { + if (!req.user?.tenant_id) { + return reply.code(400).send({ error: "No tenant selected" }) + } + + const { resource } = req.params as { resource: string } + + // ❌ Wenn falsche Ressource + if (!TABLE_MAP[resource]) { + return reply.code(400).send({ error: "Invalid special resource" }) + } + + const table = TABLE_MAP[resource] + + const { select, sort, asc: ascQuery } = req.query as { + select?: string + sort?: string + asc?: string + } + + // --------------------------------------- + // 📌 SELECT: wir ignorieren select string (wie Supabase) + // Drizzle kann kein dynamisches Select aus String! + // Wir geben IMMER alle Spalten zurück → kompatibel zum Frontend + // --------------------------------------- + + let query = server.db.select().from(table) + + // --------------------------------------- + // 📌 Sortierung + // --------------------------------------- + if (sort) { + const col = (table as any)[sort] + if (col) { + //@ts-ignore + query = + ascQuery === "true" + ? query.orderBy(asc(col)) + : query.orderBy(desc(col)) + } + } + + const data = await query + + // Falls sort clientseitig wie früher notwendig ist: + const sorted = sortData( + data, + sort, + ascQuery === "true" + ) + + return sorted + } + catch (err) { + console.error(err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) +} diff --git a/src/routes/staff/time.ts b/src/routes/staff/time.ts new file mode 100644 index 0000000..d1c3a58 --- /dev/null +++ b/src/routes/staff/time.ts @@ -0,0 +1,430 @@ +import { FastifyInstance } from "fastify" +import { + stafftimeentries, + stafftimenetryconnects +} from "../../../db/schema" +import { + eq, + and, + gte, + lte, + desc +} from "drizzle-orm" +import {stafftimeevents} from "../../../db/schema/staff_time_events"; +import {loadRelatedAdminEvents, loadValidEvents} from "../../modules/time/loadvalidevents.service"; +import {deriveTimeSpans} from "../../modules/time/derivetimespans.service"; +import {buildTimeEvaluationFromSpans} from "../../modules/time/buildtimeevaluation.service"; +import {z} from "zod"; +import {enrichSpansWithStatus} from "../../modules/time/enrichtimespanswithstatus.service"; + +export default async function staffTimeRoutes(server: FastifyInstance) { + + + server.post("/staff/time/event", async (req, reply) => { + try { + const userId = req.user.user_id + const tenantId = req.user.tenant_id + + const body = req.body as any + + const normalizeDate = (val: any) => { + if (!val) return null + const d = new Date(val) + return isNaN(d.getTime()) ? null : d + } + + const dataToInsert = { + tenant_id: tenantId, + user_id: userId, + actortype: "user", + actoruser_id: userId, + eventtime: normalizeDate(body.eventtime), + eventtype: body.eventtype, + source: "WEB", + payload: body.payload // Payload (z.B. Description) mit speichern + } + + console.log(dataToInsert) + + const [created] = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(dataToInsert) + .returning() + + return created + } catch (err: any) { + console.error(err) + return reply.code(400).send({ error: err.message }) + } + }) + + // 🆕 POST /staff/time/edit (Bearbeiten durch Invalidieren + Neu erstellen) + server.post("/staff/time/edit", async (req, reply) => { + try { + const userId = req.user.user_id; + const tenantId = req.user.tenant_id; + + // Wir erwarten das komplette Paket für die Änderung + const { + originalEventIds, // Array der IDs, die "gelöscht" werden sollen (Start ID, End ID) + newStart, // ISO String + newEnd, // ISO String + newType, // z.B. 'work', 'vacation' + description, + reason // Warum wurde geändert? (Audit) + } = req.body as { + originalEventIds: string[], + newStart: string, + newEnd: string | null, + newType: string, + description?: string, + reason?: string + }; + + if (!originalEventIds || originalEventIds.length === 0) { + return reply.code(400).send({ error: "Keine Events zum Bearbeiten angegeben." }); + } + + // 1. Transaction starten (damit alles oder nichts passiert) + await server.db.transaction(async (tx) => { + + // A. INVALIDIEREN (Die alten Events "löschen") + // Wir erstellen für jedes alte Event ein 'invalidated' Event + const invalidations = originalEventIds.map(id => ({ + tenant_id: tenantId, + user_id: userId, // Gehört dem Mitarbeiter + actortype: "user", + actoruser_id: userId, // Wer hat geändert? + eventtime: new Date(), + eventtype: "invalidated", // <--- NEUER TYP: Muss in loadValidEvents gefiltert werden! + source: "WEB", + related_event_id: id, // Zeigt auf das alte Event + metadata: { + reason: reason || "Bearbeitung", + replaced_by_edit: true + } + })); + + // Batch Insert + // @ts-ignore + await tx.insert(stafftimeevents).values(invalidations); + + // B. NEU ERSTELLEN (Die korrigierten Events anlegen) + + // Start Event + // @ts-ignore + await tx.insert(stafftimeevents).values({ + tenant_id: tenantId, + user_id: userId, + actortype: "user", + actoruser_id: userId, + eventtime: new Date(newStart), + eventtype: `${newType}_start`, // z.B. work_start + source: "WEB", + payload: { description: description || "" } + }); + + // End Event (nur wenn vorhanden) + if (newEnd) { + // @ts-ignore + await tx.insert(stafftimeevents).values({ + tenant_id: tenantId, + user_id: userId, + actortype: "user", + actoruser_id: userId, + eventtime: new Date(newEnd), + eventtype: `${newType}_end`, // z.B. work_end + source: "WEB" + }); + } + }); + + return { success: true }; + + } catch (err: any) { + console.error("Fehler beim Bearbeiten:", err); + return reply.code(500).send({ error: err.message }); + } + }); + + // POST /staff/time/submit + server.post("/staff/time/submit", async (req, reply) => { + try { + const userId = req.user.user_id; // Mitarbeiter, der einreicht + const tenantId = req.user.tenant_id; + + // Erwartet eine Liste von IDs der faktischen Events (work_start, work_end, etc.) + const { eventIds } = req.body as { eventIds: string[] }; + + if (eventIds.length === 0) { + return reply.code(400).send({ error: "Keine Events zum Einreichen angegeben." }); + } + + const inserts = eventIds.map((eventId) => ({ + tenant_id: tenantId, + user_id: userId, // Event gehört zum Mitarbeiter + actortype: "user", + actoruser_id: userId, // Mitarbeiter ist der Akteur + eventtime: new Date(), + eventtype: "submitted", // NEU: Event-Typ für Einreichung + source: "WEB", + related_event_id: eventId, // Verweis auf das faktische Event + })); + + const createdEvents = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(inserts) + .returning(); + + return { submittedCount: createdEvents.length }; + } catch (err: any) { + console.error(err); + return reply.code(500).send({ error: err.message }); + } + }); + + // POST /staff/time/approve + server.post("/staff/time/approve", async (req, reply) => { + try { + // 🚨 Berechtigungsprüfung (Voraussetzung: req.user enthält Manager-Status) + /*if (!req.user.isManager) { + return reply.code(403).send({ error: "Keine Genehmigungsberechtigung." }); + }*/ + + const actorId = req.user.user_id; // Manager ist der Akteur + const tenantId = req.user.tenant_id; + + const { eventIds, employeeUserId } = req.body as { + eventIds: string[]; + employeeUserId: string; // Die ID des Mitarbeiters, dessen Zeit genehmigt wird + }; + + if (eventIds.length === 0) { + return reply.code(400).send({ error: "Keine Events zur Genehmigung angegeben." }); + } + + const inserts = eventIds.map((eventId) => ({ + tenant_id: tenantId, + user_id: employeeUserId, // Event gehört zum Mitarbeiter + actortype: "user", + actoruser_id: actorId, // Manager ist der Akteur + eventtime: new Date(), + eventtype: "approved", // NEU: Event-Typ für Genehmigung + source: "WEB", + related_event_id: eventId, // Verweis auf das faktische Event + metadata: { + // Optional: Genehmigungskommentar + approvedBy: req.user.email + } + })); + + const createdEvents = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(inserts) + .returning(); + + return { approvedCount: createdEvents.length }; + } catch (err: any) { + console.error(err); + return reply.code(500).send({ error: err.message }); + } + }); + + // POST /staff/time/reject + server.post("/staff/time/reject", async (req, reply) => { + try { + // 🚨 Berechtigungsprüfung + /*if (!req.user.isManager) { + return reply.code(403).send({ error: "Keine Zurückweisungsberechtigung." }); + }*/ + + const actorId = req.user.user_id; // Manager ist der Akteur + const tenantId = req.user.tenant_id; + + const { eventIds, employeeUserId, reason } = req.body as { + eventIds: string[]; + employeeUserId: string; + reason?: string; // Optionaler Grund für die Ablehnung + }; + + if (eventIds.length === 0) { + return reply.code(400).send({ error: "Keine Events zur Ablehnung angegeben." }); + } + + const inserts = eventIds.map((eventId) => ({ + tenant_id: tenantId, + user_id: employeeUserId, // Event gehört zum Mitarbeiter + actortype: "user", + actoruser_id: actorId, // Manager ist der Akteur + eventtime: new Date(), + eventtype: "rejected", // NEU: Event-Typ für Ablehnung + source: "WEB", + related_event_id: eventId, // Verweis auf das faktische Event + metadata: { + reason: reason || "Ohne Angabe" + } + })); + + const createdEvents = await server.db + .insert(stafftimeevents) + //@ts-ignore + .values(inserts) + .returning(); + + return { rejectedCount: createdEvents.length }; + } catch (err: any) { + console.error(err); + return reply.code(500).send({ error: err.message }); + } + }); + + // GET /api/staff/time/spans + server.get("/staff/time/spans", async (req, reply) => { + try { + // Der eingeloggte User (Anfragesteller) + const actingUserId = req.user.user_id; + const tenantId = req.user.tenant_id; + + // Query-Parameter: targetUserId ist optional + const { targetUserId } = req.query as { targetUserId?: string }; + + // Falls eine targetUserId übergeben wurde, nutzen wir diese, sonst die eigene ID + const evaluatedUserId = targetUserId || actingUserId; + + // 💡 "Unendlicher" Zeitraum, wie gewünscht + const startDate = new Date(0); // 1970 + const endDate = new Date("2100-12-31"); + + // SCHRITT 1: Lade ALLE Events für den ZIEL-USER (evaluatedUserId) + // WICHTIG: loadValidEvents muss "invalidated" Events und deren related_ids ausfiltern! + const allEventsInTimeFrame = await loadValidEvents( + server, + tenantId, + evaluatedUserId, // <--- Hier wird jetzt die variable ID genutzt + startDate, + endDate + ); + + // SCHRITT 2: Filtere faktische Events + const FACTUAL_EVENT_TYPES = new Set([ + "work_start", "work_end", "pause_start", "pause_end", + "sick_start", "sick_end", "vacation_start", "vacation_end", + "overtime_compensation_start", "overtime_compensation_end", + "auto_stop" + ]); + const factualEvents = allEventsInTimeFrame.filter(e => FACTUAL_EVENT_TYPES.has(e.eventtype)); + + // SCHRITT 3: Hole administrative Events + const factualEventIds = factualEvents.map(e => e.id); + + if (factualEventIds.length === 0) { + return []; + } + + const relatedAdminEvents = await loadRelatedAdminEvents(server, factualEventIds); + + // SCHRITT 4: Kombinieren und Sortieren + const combinedEvents = [ + ...factualEvents, + ...relatedAdminEvents, + ].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime()); + + // SCHRITT 5: Spans ableiten + const derivedSpans = deriveTimeSpans(combinedEvents); + + // SCHRITT 6: Spans anreichern + const enrichedSpans = enrichSpansWithStatus(derivedSpans, combinedEvents); + + return enrichedSpans; + + } catch (error) { + console.error("Fehler beim Laden der Spans:", error); + return reply.code(500).send({ error: "Interner Fehler beim Laden der Zeitspannen." }); + } + }); + + server.get("/staff/time/evaluation", async (req, reply) => { + try { + // --- 1. Eingangsdaten und Validierung des aktuellen Nutzers --- + + // Daten des aktuell eingeloggten (anfragenden) Benutzers + const actingUserId = req.user.user_id; + const tenantId = req.user.tenant_id; + + // Query-Parameter extrahieren + const { from, to, targetUserId } = req.query as { + from: string, + to: string, + targetUserId?: string // Optionale ID des Benutzers, dessen Daten abgerufen werden sollen + }; + + // Die ID, für die die Auswertung tatsächlich durchgeführt wird + const evaluatedUserId = targetUserId || actingUserId; + + const startDate = new Date(from); + const endDate = new Date(to); + + if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + return reply.code(400).send({ error: "Ungültiges Datumsformat." }); + } + + // --- 3. Ausführung der Logik für den ermittelten Benutzer --- + + // SCHRITT 1: Lade ALLE gültigen Events im Zeitraum + // WICHTIG: loadValidEvents muss "invalidated" Events und deren related_ids ausfiltern! + const allEventsInTimeFrame = await loadValidEvents( + server, tenantId, evaluatedUserId, startDate, endDate // Verwendung der evaluatedUserId + ); + + // 1b: Trenne Faktische und Administrative Events + const FACTUAL_EVENT_TYPES = new Set([ + "work_start", "work_end", "pause_start", "pause_end", + "sick_start", "sick_end", "vacation_start", "vacation_end", + "overtime_compensation_start", "overtime_compensation_end", + "auto_stop" + ]); + const factualEvents = allEventsInTimeFrame.filter(e => FACTUAL_EVENT_TYPES.has(e.eventtype)); + + // 1c: Sammle alle IDs der faktischen Events im Zeitraum + const factualEventIds = factualEvents.map(e => e.id); + + // SCHRITT 2: Lade die administrativen Events, die sich auf diese IDs beziehen (auch NACH dem Zeitraum) + const relatedAdminEvents = await loadRelatedAdminEvents(server, factualEventIds); + + // SCHRITT 3: Kombiniere alle Events für die Weiterverarbeitung + const combinedEvents = [ + ...factualEvents, + ...relatedAdminEvents, + ].sort((a, b) => a.eventtime.getTime() - b.eventtime.getTime()); + + // SCHRITT 4: Ableiten und Anreichern + const derivedSpans = deriveTimeSpans(combinedEvents); + const enrichedSpans = enrichSpansWithStatus(derivedSpans, combinedEvents); + + // SCHRITT 5: Erstellung der finalen Auswertung (Summen und Salden) + const evaluationSummary = await buildTimeEvaluationFromSpans( + server, + evaluatedUserId, // Verwendung der evaluatedUserId + tenantId, + from, + to, + enrichedSpans + ); + + return { + userId: evaluatedUserId, // Rückgabe der ID, für die ausgewertet wurde + spans: enrichedSpans, + summary: evaluationSummary + }; + + + } catch (error) { + console.error("Fehler in /staff/time/evaluation:", error); + return reply.code(500).send({ error: "Interner Serverfehler bei der Zeitauswertung." }); + } + }); + +} \ No newline at end of file diff --git a/src/routes/staff/timeconnects.ts b/src/routes/staff/timeconnects.ts new file mode 100644 index 0000000..288e341 --- /dev/null +++ b/src/routes/staff/timeconnects.ts @@ -0,0 +1,71 @@ +import { FastifyInstance } from 'fastify' +import { StaffTimeEntryConnect } from '../../types/staff' + +export default async function staffTimeConnectRoutes(server: FastifyInstance) { + + // ▶ Connect anlegen + server.post<{ Params: { id: string }, Body: Omit }>( + '/staff/time/:id/connects', + async (req, reply) => { + const { id } = req.params + const { started_at, stopped_at, project_id, customer_id, task_id, ticket_id, notes } = req.body + + const { data, error } = await server.supabase + .from('staff_time_entry_connects') + .insert([{ time_entry_id: id, started_at, stopped_at, project_id, customer_id, task_id, ticket_id, notes }]) + .select() + .maybeSingle() + + if (error) return reply.code(400).send({ error: error.message }) + return reply.send(data) + } + ) + + // ▶ Connects abrufen + server.get<{ Params: { id: string } }>( + '/staff/time/:id/connects', + async (req, reply) => { + const { id } = req.params + const { data, error } = await server.supabase + .from('staff_time_entry_connects') + .select('*') + .eq('time_entry_id', id) + .order('started_at', { ascending: true }) + + if (error) return reply.code(400).send({ error: error.message }) + return reply.send(data) + } + ) + + // ▶ Connect aktualisieren + server.patch<{ Params: { connectId: string }, Body: Partial }>( + '/staff/time/connects/:connectId', + async (req, reply) => { + const { connectId } = req.params + const { data, error } = await server.supabase + .from('staff_time_entry_connects') + .update({ ...req.body, updated_at: new Date().toISOString() }) + .eq('id', connectId) + .select() + .maybeSingle() + + if (error) return reply.code(400).send({ error: error.message }) + return reply.send(data) + } + ) + + // ▶ Connect löschen + server.delete<{ Params: { connectId: string } }>( + '/staff/time/connects/:connectId', + async (req, reply) => { + const { connectId } = req.params + const { error } = await server.supabase + .from('staff_time_entry_connects') + .delete() + .eq('id', connectId) + + if (error) return reply.code(400).send({ error: error.message }) + return reply.send({ success: true }) + } + ) +} diff --git a/src/routes/tenant.ts b/src/routes/tenant.ts new file mode 100644 index 0000000..93fa7c6 --- /dev/null +++ b/src/routes/tenant.ts @@ -0,0 +1,244 @@ +import { FastifyInstance } from "fastify" +import jwt from "jsonwebtoken" +import { secrets } from "../utils/secrets" + +import { + authTenantUsers, + authUsers, + authProfiles, + tenants +} from "../../db/schema" + +import {and, eq, inArray} from "drizzle-orm" + + +export default async function tenantRoutes(server: FastifyInstance) { + + + // ------------------------------------------------------------- + // GET CURRENT TENANT + // ------------------------------------------------------------- + server.get("/tenant", async (req) => { + if (req.tenant) { + return { + message: `Hallo vom Tenant ${req.tenant?.name}`, + tenant_id: req.tenant?.id, + } + } + return { + message: "Server ist im MultiTenant-Modus – es werden alle verfügbaren Tenants geladen." + } + }) + + + + // ------------------------------------------------------------- + // SWITCH TENANT + // ------------------------------------------------------------- + server.post("/tenant/switch", async (req, reply) => { + try { + if (!req.user) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + const { tenant_id } = req.body as { tenant_id: string } + if (!tenant_id) return reply.code(400).send({ error: "tenant_id required" }) + + // prüfen ob der User zu diesem Tenant gehört + const membership = await server.db + .select() + .from(authTenantUsers) + .where(and( + eq(authTenantUsers.user_id, req.user.user_id), + eq(authTenantUsers.tenant_id, Number(tenant_id)) + )) + + if (!membership.length) { + return reply.code(403).send({ error: "Not a member of this tenant" }) + } + + // JWT neu erzeugen + const token = jwt.sign( + { + user_id: req.user.user_id, + email: req.user.email, + tenant_id, + }, + secrets.JWT_SECRET!, + { expiresIn: "6h" } + ) + + reply.setCookie("token", token, { + path: "/", + httpOnly: true, + sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", + secure: process.env.NODE_ENV === "production", + maxAge: 60 * 60 * 3, + }) + + return { token } + + } catch (err) { + console.error("TENANT SWITCH ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // TENANT USERS (auth_users + auth_profiles) + // ------------------------------------------------------------- + server.get("/tenant/users", async (req, reply) => { + try { + const authUser = req.user + if (!authUser) return reply.code(401).send({ error: "Unauthorized" }) + + const tenantId = authUser.tenant_id + + // 1) auth_tenant_users → user_ids + const tenantUsers = await server.db + .select() + .from(authTenantUsers) + .where(eq(authTenantUsers.tenant_id, tenantId)) + + const userIds = tenantUsers.map(u => u.user_id) + + if (!userIds.length) { + return { tenant_id: tenantId, users: [] } + } + + // 2) auth_users laden + const users = await server.db + .select() + .from(authUsers) + .where(inArray(authUsers.id, userIds)) + + // 3) auth_profiles pro Tenant laden + const profiles = await server.db + .select() + .from(authProfiles) + .where( + and( + eq(authProfiles.tenant_id, tenantId), + inArray(authProfiles.user_id, userIds) + )) + + const combined = users.map(u => { + const profile = profiles.find(p => p.user_id === u.id) + return { + id: u.id, + email: u.email, + profile, + full_name: profile?.full_name ?? null + } + }) + + return { tenant_id: tenantId, users: combined } + + } catch (err) { + console.error("/tenant/users ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // TENANT PROFILES + // ------------------------------------------------------------- + server.get("/tenant/profiles", async (req, reply) => { + try { + const tenantId = req.user?.tenant_id + if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) + + const data = await server.db + .select() + .from(authProfiles) + .where(eq(authProfiles.tenant_id, tenantId)) + + return { data } + + } catch (err) { + console.error("/tenant/profiles ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // UPDATE NUMBER RANGE + // ------------------------------------------------------------- + server.put("/tenant/numberrange/:numberrange", async (req, reply) => { + try { + const user = req.user + if (!user) return reply.code(401).send({ error: "Unauthorized" }) + + const { numberrange } = req.params as { numberrange: string } + const { numberRange } = req.body as { numberRange: any } + + if (!numberRange) { + return reply.code(400).send({ error: "numberRange required" }) + } + + const tenantId = Number(user.tenant_id) + + const currentTenantRows = await server.db + .select() + .from(tenants) + .where(eq(tenants.id, tenantId)) + + const current = currentTenantRows[0] + if (!current) return reply.code(404).send({ error: "Tenant not found" }) + + const updatedRanges = { + //@ts-ignore + ...current.numberRanges, + [numberrange]: numberRange + } + + const updated = await server.db + .update(tenants) + .set({ numberRanges: updatedRanges }) + .where(eq(tenants.id, tenantId)) + .returning() + + return updated[0] + + } catch (err) { + console.error("/tenant/numberrange ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + + + + // ------------------------------------------------------------- + // UPDATE TENANT OTHER FIELDS + // ------------------------------------------------------------- + server.put("/tenant/other/:id", async (req, reply) => { + try { + const user = req.user + if (!user) return reply.code(401).send({ error: "Unauthorized" }) + + const { id } = req.params as { id: string } + const { data } = req.body as { data: any } + + if (!data) return reply.code(400).send({ error: "data required" }) + + const updated = await server.db + .update(tenants) + .set(data) + .where(eq(tenants.id, Number(user.tenant_id))) + .returning() + + return updated[0] + + } catch (err) { + console.error("/tenant/other ERROR:", err) + return reply.code(500).send({ error: "Internal Server Error" }) + } + }) + +} diff --git a/src/types/staff.ts b/src/types/staff.ts new file mode 100644 index 0000000..b16ae42 --- /dev/null +++ b/src/types/staff.ts @@ -0,0 +1,27 @@ +export interface StaffTimeEntry { + id: string + tenant_id: string + user_id: string + started_at: string + stopped_at?: string | null + duration_minutes?: number | null + type: 'work' | 'break' | 'absence' | 'other' + description?: string | null + created_at?: string + updated_at?: string +} + +export interface StaffTimeEntryConnect { + id: string + time_entry_id: string + project_id?: string | null + customer_id?: string | null + task_id?: string | null + ticket_id?: string | null + started_at: string + stopped_at: string + duration_minutes?: number + notes?: string | null + created_at?: string + updated_at?: string +} diff --git a/src/utils/crypt.ts b/src/utils/crypt.ts new file mode 100644 index 0000000..1ed7ddd --- /dev/null +++ b/src/utils/crypt.ts @@ -0,0 +1,38 @@ +import crypto from "crypto"; +import {secrets} from "./secrets" +const ALGORITHM = "aes-256-gcm"; + + + + +export function encrypt(text) { + const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex"); + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv); + + const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + + return { + iv: iv.toString("hex"), + content: encrypted.toString("hex"), + tag: tag.toString("hex"), + }; +} + +export function decrypt({ iv, content, tag }) { + const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex"); + const decipher = crypto.createDecipheriv( + ALGORITHM, + ENCRYPTION_KEY, + Buffer.from(iv, "hex") + ); + decipher.setAuthTag(Buffer.from(tag, "hex")); + + const decrypted = Buffer.concat([ + decipher.update(Buffer.from(content, "hex")), + decipher.final(), + ]); + + return decrypted.toString("utf8"); +} diff --git a/src/utils/dbSearch.ts b/src/utils/dbSearch.ts new file mode 100644 index 0000000..e306167 --- /dev/null +++ b/src/utils/dbSearch.ts @@ -0,0 +1,27 @@ +import { ilike, or } from "drizzle-orm" + +/** + * Erzeugt eine OR-Suchbedingung über mehrere Spalten + * + * @param table - Drizzle Table Schema + * @param columns - Array der Spaltennamen (property names im schema) + * @param search - Suchbegriff + */ +export function buildSearchWhere(table: any, columns: string[], search: string) { + if (!search || !columns.length) return undefined + + const term = `%${search.toLowerCase()}%` + + const parts = columns + .map((colName) => { + const col = table[colName] + if (!col) return null + return ilike(col, term) + }) + .filter(Boolean) + + if (parts.length === 0) return undefined + + // @ts-ignore + return or(...parts) +} diff --git a/src/utils/diff.ts b/src/utils/diff.ts new file mode 100644 index 0000000..947372c --- /dev/null +++ b/src/utils/diff.ts @@ -0,0 +1,103 @@ + +import {diffTranslations} from "./diffTranslations"; + +export type DiffChange = { + key: string; + label: string; + oldValue: any; + newValue: any; + type: "created" | "updated" | "deleted" | "unchanged"; + typeLabel: "erstellt" | "geändert" | "gelöscht" | "unverändert"; +}; + +const IGNORED_KEYS = new Set([ + "updated_at", + "updated_by", + "created_at", + "created_by", + "id", + "phases" +]); + +/** + * Vergleicht zwei Objekte und gibt die Änderungen zurück. + * @param obj1 Altes Objekt + * @param obj2 Neues Objekt + * @param ctx Lookup-Objekte (z. B. { projects, customers, vendors, profiles, plants }) + */ +export function diffObjects( + obj1: Record, + obj2: Record, + ctx: Record = {} +): DiffChange[] { + const diffs: DiffChange[] = []; + + const allKeys = new Set([ + ...Object.keys(obj1 || {}), + ...Object.keys(obj2 || {}), + ]); + + for (const key of allKeys) { + if (IGNORED_KEYS.has(key)) continue; // Felder überspringen + + const oldVal = obj1?.[key]; + const newVal = obj2?.[key]; + + console.log(oldVal, key, newVal); + + // Wenn beides null/undefined → ignorieren + if ( + (oldVal === null || oldVal === undefined || oldVal === "" || JSON.stringify(oldVal) === "[]") && + (newVal === null || newVal === undefined || newVal === "" || JSON.stringify(newVal) === "[]") + ) { + continue; + } + + let type: DiffChange["type"] = "unchanged"; + let typeLabel: DiffChange["typeLabel"] = "unverändert"; + if (oldVal === newVal) { + type = "unchanged"; + typeLabel = "unverändert"; + } else if (oldVal === undefined) { + type = "created"; + typeLabel = "erstellt" + } else if (newVal === undefined) { + type = "deleted"; + typeLabel = "gelöscht" + } else { + type = "updated"; + typeLabel = "geändert" + } + + if (type === "unchanged") continue; + + const translation = diffTranslations[key]; + let label = key; + let resolvedOld = oldVal; + let resolvedNew = newVal; + + if (translation) { + label = translation.label; + if (translation.resolve) { + const { oldVal: resOld, newVal: resNew } = translation.resolve( + oldVal, + newVal, + ctx + ); + resolvedOld = resOld; + resolvedNew = resNew; + } + } + + diffs.push({ + key, + label, + typeLabel, + oldValue: resolvedOld ?? "-", + newValue: resolvedNew ?? "-", + type, + }); + } + + return diffs; +} \ No newline at end of file diff --git a/src/utils/diffTranslations.ts b/src/utils/diffTranslations.ts new file mode 100644 index 0000000..290ef38 --- /dev/null +++ b/src/utils/diffTranslations.ts @@ -0,0 +1,165 @@ +import dayjs from "dayjs"; + +type ValueResolver = ( + oldVal: any, + newVal: any, + ctx?: Record +) => { oldVal: any; newVal: any }; + +export const diffTranslations: Record< + string, + { label: string; resolve?: ValueResolver } +> = { + project: { + label: "Projekt", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.projects?.find((i: any) => i.id === o)?.name ?? "-" : "-", + newVal: n ? ctx?.projects?.find((i: any) => i.id === n)?.name ?? "-" : "-", + }), + }, + title: { label: "Titel" }, + type: { label: "Typ" }, + notes: { label: "Notizen" }, + link: { label: "Link" }, + + start: { + label: "Start", + resolve: (o, n) => ({ + oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-", + newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-", + }), + }, + end: { + label: "Ende", + resolve: (o, n) => ({ + oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-", + newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-", + }), + }, + birthday: { + label: "Geburtstag", + resolve: (o, n) => ({ + oldVal: o ? dayjs(o).format("DD.MM.YYYY") : "-", + newVal: n ? dayjs(n).format("DD.MM.YYYY") : "-", + }), + }, + resources: { + label: "Resourcen", + resolve: (o, n) => ({ + oldVal: Array.isArray(o) ? o.map((i: any) => i.title).join(", ") : "-", + newVal: Array.isArray(n) ? n.map((i: any) => i.title).join(", ") : "-", + }), + }, + + customerNumber: { label: "Kundennummer" }, + active: { + label: "Aktiv", + resolve: (o, n) => ({ + oldVal: o === true ? "Aktiv" : "Gesperrt", + newVal: n === true ? "Aktiv" : "Gesperrt", + }), + }, + isCompany: { + label: "Firmenkunde", + resolve: (o, n) => ({ + oldVal: o === true ? "Firma" : "Privatkunde", + newVal: n === true ? "Firma" : "Privatkunde", + }), + }, + special: { label: "Adresszusatz" }, + street: { label: "Straße & Hausnummer" }, + city: { label: "Ort" }, + zip: { label: "Postleitzahl" }, + country: { label: "Land" }, + web: { label: "Webseite" }, + email: { label: "E-Mail" }, + tel: { label: "Telefon" }, + ustid: { label: "USt-ID" }, + role: { label: "Rolle" }, + phoneHome: { label: "Festnetz" }, + phoneMobile: { label: "Mobiltelefon" }, + salutation: { label: "Anrede" }, + firstName: { label: "Vorname" }, + lastName: { label: "Nachname" }, + name: { label: "Name" }, + nameAddition: { label: "Name Zusatz" }, + approved: { label: "Genehmigt" }, + manufacturer: { label: "Hersteller" }, + purchasePrice: { label: "Kaufpreis" }, + purchaseDate: { label: "Kaufdatum" }, + serialNumber: { label: "Seriennummer" }, + usePlanning: { label: "In Plantafel verwenden" }, + currentSpace: { label: "Lagerplatz" }, + + customer: { + label: "Kunde", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.customers?.find((i: any) => i.id === o)?.name ?? "-" : "-", + newVal: n ? ctx?.customers?.find((i: any) => i.id === n)?.name ?? "-" : "-", + }), + }, + vendor: { + label: "Lieferant", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.vendors?.find((i: any) => i.id === o)?.name ?? "-" : "-", + newVal: n ? ctx?.vendors?.find((i: any) => i.id === n)?.name ?? "-" : "-", + }), + }, + + description: { label: "Beschreibung" }, + categorie: { label: "Kategorie" }, + + profile: { + label: "Mitarbeiter", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-", + newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-", + }), + }, + plant: { + label: "Objekt", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.plants?.find((i: any) => i.id === o)?.name ?? "-" : "-", + newVal: n ? ctx?.plants?.find((i: any) => i.id === n)?.name ?? "-" : "-", + }), + }, + + annualPaidLeaveDays: { label: "Urlaubstage" }, + employeeNumber: { label: "Mitarbeiternummer" }, + weeklyWorkingDays: { label: "Wöchentliche Arbeitstage" }, + weeklyWorkingHours: { label: "Wöchentliche Arbeitszeit" }, + customerRef: { label: "Referenz des Kunden" }, + + licensePlate: { label: "Kennzeichen" }, + tankSize: { label: "Tankvolumen" }, + towingCapacity: { label: "Anhängelast" }, + color: { label: "Farbe" }, + customPaymentDays: { label: "Zahlungsziel in Tagen" }, + customSurchargePercentage: { label: "Individueller Aufschlag" }, + powerInKW: { label: "Leistung" }, + + driver: { + label: "Fahrer", + resolve: (o, n, ctx) => ({ + oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-", + newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-", + }), + }, + + projecttype: { label: "Projekttyp" }, + + fixed: { + label: "Festgeschrieben", + resolve: (o, n) => ({ + oldVal: o === true ? "Ja" : "Nein", + newVal: n === true ? "Ja" : "Nein", + }), + }, + archived: { + label: "Archiviert", + resolve: (o, n) => ({ + oldVal: o === true ? "Ja" : "Nein", + newVal: n === true ? "Ja" : "Nein", + }), + }, +}; diff --git a/src/utils/emailengine.ts b/src/utils/emailengine.ts new file mode 100644 index 0000000..705a399 --- /dev/null +++ b/src/utils/emailengine.ts @@ -0,0 +1,45 @@ +import axios from "axios" + +const AxiosEE = axios.create({ + baseURL: process.env.EMAILENGINE_URL ||"https://ee.fedeo.io/v1", + headers: { + Authorization: `Bearer ${process.env.EMAILENGINE_TOKEN || "dcd8209bc5371c728f9ec951600afcfc74e8c391a7e984b2a6df9c4665dc7ad6"}`, + Accept: "application/json", + }, +}) + + + +export async function sendMailAsUser( + to: string, + subject: string, + html: string, + text: string, + account: string, + cc: string, + bcc: string, + attachments: any, +): Promise<{ success: boolean; info?: any; error?: any }> { + try { + const sendData = { + to: to.split(";").map(i => { return {address: i}}), + cc: cc ? cc.split(";").map((i:any) => { return {address: i}}) : null, + bcc: bcc ? bcc.split(";").map((i:any) => { return {address: i}}) : null, + subject, + text, + html, + attachments + } + + if(sendData.cc === null) delete sendData.cc + if(sendData.bcc === null) delete sendData.bcc + + const {data} = await AxiosEE.post(`/account/${account}/submit`, sendData) + + return { success: true, info: data } + + } catch (err) { + console.error("❌ Fehler beim Mailversand:", err) + return { success: false, error: err } + } +} \ No newline at end of file diff --git a/src/utils/export/datev.ts b/src/utils/export/datev.ts new file mode 100644 index 0000000..8c9efa6 --- /dev/null +++ b/src/utils/export/datev.ts @@ -0,0 +1,388 @@ +import xmlbuilder from "xmlbuilder"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween.js" +import {BlobWriter, Data64URIReader, TextReader, TextWriter, ZipWriter} from "@zip.js/zip.js"; +import {FastifyInstance} from "fastify"; +import {GetObjectCommand} from "@aws-sdk/client-s3"; +import {s3} from "../s3"; +import {secrets} from "../secrets"; +dayjs.extend(isBetween) + +const getCreatedDocumentTotal = (item) => { + let totalNet = 0 + let total19 = 0 + let total7 = 0 + + item.rows.forEach(row => { + if(!['pagebreak','title','text'].includes(row.mode)){ + let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3) + totalNet = totalNet + Number(rowPrice) + + if(row.taxPercent === 19) { + // @ts-ignore + total19 = total19 + Number(rowPrice * 0.19) + } else if(row.taxPercent === 7) { + // @ts-ignore + total7 = total7 + Number(rowPrice * 0.07) + } + } + }) + + let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2)) + + + + return { + totalNet: totalNet, + total19: total19, + total7: total7, + totalGross: totalGross, + } +} + +const escapeString = (str) => { + + str = (str ||"") + .replaceAll("\n","") + .replaceAll(";","") + .replaceAll(/\r/g,"") + .replaceAll(/"/g,"") + .replaceAll(/ü/g,"ue") + .replaceAll(/ä/g,"ae") + .replaceAll(/ö/g,"oe") + return str +} + +const displayCurrency = (input, onlyAbs = false) => { + + if(onlyAbs) { + return Math.abs(input).toFixed(2).replace(".",",") + } else { + return input.toFixed(2).replace(".",",") + } +} + +export async function buildExportZip(server: FastifyInstance, tenant: number, startDate: string, endDate: string, beraternr: string, mandantennr: string): Promise { + + try { + const zipFileWriter = new BlobWriter() + const zipWriter = new ZipWriter(zipFileWriter) + + + + //Basic Information + + let header = `"EXTF";700;21;"Buchungsstapel";13;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto` + + //Get Bookings + const {data:statementallocationsRaw,error: statementallocationsError} = await server.supabase.from("statementallocations").select('*, account(*), bs_id(*, account(*)), cd_id(*,customer(*)), ii_id(*, vendor(*)), vendor(*), customer(*), ownaccount(*)').eq("tenant", tenant); + let {data:createddocumentsRaw,error: createddocumentsError} = await server.supabase.from("createddocuments").select('*,customer(*)').eq("tenant", tenant).in("type",["invoices","advanceInvoices","cancellationInvoices"]).eq("state","Gebucht").eq("archived",false) + let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant).eq("state","Gebucht").eq("archived",false) + const {data:accounts} = await server.supabase.from("accounts").select() + const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single() + + let createddocuments = createddocumentsRaw.filter(i => dayjs(i.documentDate).isBetween(startDate,endDate,"day","[]")) + let incominginvoices = incominginvoicesRaw.filter(i => dayjs(i.date).isBetween(startDate,endDate,"day","[]")) + let statementallocations = statementallocationsRaw.filter(i => dayjs(i.bs_id.date).isBetween(startDate,endDate,"day","[]")) + + + const {data:filesCreateddocuments, error: filesErrorCD} = await server.supabase.from("files").select().eq("tenant",tenant).or(`createddocument.in.(${createddocuments.map(i => i.id).join(",")})`) + const {data:filesIncomingInvoices, error: filesErrorII} = await server.supabase.from("files").select().eq("tenant",tenant).or(`incominginvoice.in.(${incominginvoices.map(i => i.id).join(",")})`) + + const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => { + + console.log(filePath) + + const command = new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: filePath, + }) + + const { Body, ContentType } = await s3.send(command) + + const chunks: any[] = [] + // @ts-ignore + for await (const chunk of Body) { + chunks.push(chunk) + } + const buffer = Buffer.concat(chunks) + + const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}` + + const dataURLReader = new Data64URIReader(dataURL) + await zipWriter.add(`${fileId}.${downloadFilePath.split(".").pop()}`, dataURLReader) + + //await fs.writeFile(`./output/${fileId}.${downloadFilePath.split(".").pop()}`, buffer, () => {}); + console.log(`File added to Zip`); + }; + + for (const file of filesCreateddocuments) { + await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id); + } + for (const file of filesIncomingInvoices) { + await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id); + } + + let bookingLines = [] + + createddocuments.forEach(createddocument => { + + let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id); + + let total = 0 + let typeString = "" + + if(createddocument.type === "invoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + + console.log() + if(createddocument.usedAdvanceInvoices.length > 0){ + createddocument.usedAdvanceInvoices.forEach(usedAdvanceInvoice => { + total -= getCreatedDocumentTotal(createddocumentsRaw.find(i => i.id === usedAdvanceInvoice)).totalGross + }) + } + + console.log(total) + + typeString = "AR" + } else if(createddocument.type === "advanceInvoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + typeString = "ARAbschlag" + } else if(createddocument.type === "cancellationInvoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + typeString = "ARStorno" + } + + let shSelector = "S" + if(Math.sign(total) === 1) { + shSelector = "S" + } else if (Math.sign(total) === -1) { + shSelector = "H" + } + + bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${createddocument.customer.customerNumber};8400;"";${dayjs(createddocument.documentDate).format("DDMM")};"${createddocument.documentNumber}";;;"${`${typeString} ${createddocument.documentNumber} - ${createddocument.customer.name}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${createddocument.customer.name}";"Kundennummer";"${createddocument.customer.customerNumber}";"Belegnummer";"${createddocument.documentNumber}";"Leistungsdatum";"${dayjs(createddocument.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(createddocument.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + + }) + + incominginvoices.forEach(incominginvoice => { + console.log(incominginvoice.id); + incominginvoice.accounts.forEach(account => { + + let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id); + + + let accountData = accounts.find(i => i.id === account.account) + let buschluessel: string = "9" + + if(account.taxType === '19'){ + buschluessel = "9" + } else if(account.taxType === 'null') { + buschluessel = "" + } else if(account.taxType === '7') { + buschluessel = "8" + } else if(account.taxType === '19I') { + buschluessel = "19" + } else if(account.taxType === '7I') { + buschluessel = "18" + } else { + buschluessel = "-" + } + + let shSelector = "S" + let amountGross = account.amountGross ? account.amountGross : account.amountNet + account.amountTax + + + if(Math.sign(amountGross) === 1) { + shSelector = "S" + } else if(Math.sign(amountGross) === -1) { + shSelector = "H" + } + + let text = `ER ${incominginvoice.reference}: ${escapeString(incominginvoice.description)}`.substring(0,59) + console.log(incominginvoice) + bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${incominginvoice.vendor.vendorNumber};"${buschluessel}";${dayjs(incominginvoice.date).format("DDMM")};"${incominginvoice.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${incominginvoice.vendor.name}";"Kundennummer";"${incominginvoice.vendor.vendorNumber}";"Belegnummer";"${incominginvoice.reference}";"Leistungsdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + }) + + + }) + + statementallocations.forEach(statementallocation => { + + let shSelector = "S" + + if(Math.sign(statementallocation.amount) === 1) { + shSelector = "S" + } else if(Math.sign(statementallocation.amount) === -1) { + shSelector = "H" + } + + if(statementallocation.cd_id) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"H";;;;;${statementallocation.cd_id.customer.customerNumber};${statementallocation.bs_id.account.datevNumber};"3";${dayjs(statementallocation.cd_id.documentDate).format("DDMM")};"${statementallocation.cd_id.documentNumber}";;;"${`ZE${statementallocation.description}${escapeString(statementallocation.bs_id.text)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.cd_id.customer.name}";"Kundennummer";"${statementallocation.cd_id.customer.customerNumber}";"Belegnummer";"${statementallocation.cd_id.documentNumber}";"Leistungsdatum";"${dayjs(statementallocation.cd_id.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.cd_id.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.ii_id) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ii_id.vendor.vendorNumber};"";${dayjs(statementallocation.ii_id.date).format("DDMM")};"${statementallocation.ii_id.reference}";;;"${`ZA${statementallocation.description} ${escapeString(statementallocation.bs_id.text)} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ii_id.vendor.name}";"Kundennummer";"${statementallocation.ii_id.vendor.vendorNumber}";"Belegnummer";"${statementallocation.ii_id.reference}";"Leistungsdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.account) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.account.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.account.number} - ${escapeString(statementallocation.account.label)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.bs_id.credName}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.vendor) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.vendor.vendorNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.vendor.vendorNumber} - ${escapeString(statementallocation.vendor.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.vendor.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.customer) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.customer.customerNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.customer.customerNumber} - ${escapeString(statementallocation.customer.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.customer.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.ownaccount) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ownaccount.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.ownaccount.number} - ${escapeString(statementallocation.ownaccount.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ownaccount.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } + + + }) + + + let csvString = `${header}\n${colHeaders}\n`; + bookingLines.forEach(line => { + csvString += `${line}\n`; + }) + + const buchungsstapelReader = new TextReader(csvString) + await zipWriter.add(`EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, buchungsstapelReader) + + /*fs.writeFile(`output/EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvString, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + // Kreditoren/Debitoren + let headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist` + const {data:customers} = await server.supabase.from("customers").select().eq("tenant",tenant).order("customerNumber") + const {data:vendors} = await server.supabase.from("vendors").select().eq("tenant",tenant).order("vendorNumber") + + let bookinglinesStammdaten = [] + + customers.forEach(customer => { + bookinglinesStammdaten.push(`${customer.customerNumber};"${customer.isCompany ? customer.name.substring(0,48): ''}";;"${!customer.isCompany ? (customer.lastname ? customer.lastname : customer.name) : ''}";"${!customer.isCompany ? (customer.firstname ? customer.firstname : '') : ''}";;${customer.isCompany ? 2 : 1};;;;;;;;"STR";"${customer.infoData.street ? customer.infoData.street : ''}";;"${customer.infoData.zip ? customer.infoData.zip : ''}";"${customer.infoData.city ? customer.infoData.city : ''}";;;"${customer.infoData.special ? customer.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`) + + }) + + vendors.forEach(vendor => { + bookinglinesStammdaten.push(`${vendor.vendorNumber};"${vendor.name.substring(0,48)}";;;;;2;;;;;;;;"STR";"${vendor.infoData.street ? vendor.infoData.street : ''}";;"${vendor.infoData.zip ? vendor.infoData.zip : ''}";"${vendor.infoData.city ? vendor.infoData.city : ''}";;;"${vendor.infoData.special ? vendor.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`) + + }) + + let csvStringStammdaten = `${headerStammdaten}\n${colHeadersStammdaten}\n`; + bookinglinesStammdaten.forEach(line => { + csvStringStammdaten += `${line}\n`; + }) + + const stammdatenReader = new TextReader(csvStringStammdaten) + await zipWriter.add(`EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, stammdatenReader) + + + + /*fs.writeFile(`output/EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringStammdaten, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + //Sachkonten + let headerSachkonten = `"EXTF";700;20;"Kontenbeschriftungen";3;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Sachkonten";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeadersSachkonten = `Konto;Kontenbeschriftung;Sprach-ID;Kontenbeschriftung lang` + const {data:bankaccounts} = await server.supabase.from("bankaccounts").select().eq("tenant",tenant).order("datevNumber") + + let bookinglinesSachkonten = [] + + bankaccounts.forEach(bankaccount => { + bookinglinesSachkonten.push(`${bankaccount.datevNumber};"${bankaccount.name}";"de-DE";`) + + }) + + let csvStringSachkonten = `${headerSachkonten}\n${colHeadersSachkonten}\n`; + bookinglinesSachkonten.forEach(line => { + csvStringSachkonten += `${line}\n`; + }) + + const sachkontenReader = new TextReader(csvStringSachkonten) + await zipWriter.add(`EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, sachkontenReader) + + /*fs.writeFile(`output/EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringSachkonten, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + + let obj = { + archive: { + '@version':"5.0", + "@generatingSystem":"fedeo.de", + "@xsi:schemaLocation":"http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd", + "@xmlns":"http://xml.datev.de/bedi/tps/document/v05.0", + "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", + header: { + date: dayjs().format("YYYY-MM-DDTHH:mm:ss") + }, + content: { + document: [] + } + } + } + + filesCreateddocuments.forEach(file => { + obj.archive.content.document.push({ + "@guid": file.id, + extension: { + "@xsi:type":"File", + "@name":`${file.id}.pdf` + } + }) + }) + + filesIncomingInvoices.forEach(file => { + obj.archive.content.document.push({ + "@guid": file.id, + extension: { + "@xsi:type":"File", + "@name":`${file.id}.pdf` + } + }) + }) + + let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true}) + + //console.log(doc.end({pretty: true})); + + const documentsReader = new TextReader(doc.end({pretty: true})) + await zipWriter.add(`document.xml`, documentsReader) + + + + + /*function toBuffer(arrayBuffer) { + const buffer = Buffer.alloc(arrayBuffer.byteLength); + const view = new Uint8Array(arrayBuffer); + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = view[i]; + } + return buffer; + }*/ + + + const arrayBuffer = await (await zipWriter.close()).arrayBuffer() + return Buffer.from(arrayBuffer) + } catch(error) { + console.log(error) + } + + +} \ No newline at end of file diff --git a/src/utils/export/sepa.ts b/src/utils/export/sepa.ts new file mode 100644 index 0000000..d13b127 --- /dev/null +++ b/src/utils/export/sepa.ts @@ -0,0 +1,114 @@ +import xmlbuilder from "xmlbuilder"; +import {randomUUID} from "node:crypto"; +import dayjs from "dayjs"; + +export const createSEPAExport = async (server,idsToExport, tenant_id) => { + const {data,error} = await server.supabase.from("createddocuments").select().eq("tenant", tenant_id).in("id", idsToExport) + const {data:tenantData,error:tenantError} = await server.supabase.from("tenants").select().eq("id", tenant_id).single() + console.log(tenantData) + console.log(tenantError) + + console.log(data) + + let transactions = [] + + let obj = { + Document: { + '@xmlns':"urn:iso:std:iso:20022:tech:xsd:pain.008.001.02", + 'CstmrDrctDbtInitn': { + 'GrpHdr': { + 'MsgId': randomUUID(), + 'CredDtTm': dayjs().format("YYYY-MM-DDTHH:mm:ss"), + 'NbOfTxs': transactions.length, + 'CtrlSum': 0, // TODO: Total Sum + 'InitgPty': { + 'Nm': tenantData.name + } + }, + 'PmtInf': { + 'PmtInfId': "", // TODO: Mandatsreferenz, + 'PmtMtd': "DD", + 'BtchBookg': "true", // TODO: BatchBooking, + 'NbOfTxs': transactions.length, + 'CtrlSum': 0, //TODO: Total Sum + 'PmtTpInf': { + 'SvcLvl': { + 'Cd': "SEPA" + }, + 'LclInstrm': { + 'Cd': "CORE" // Core für BASIS / B2B für Firmen + }, + 'SeqTp': "RCUR" // TODO: Unterscheidung Einmalig / Wiederkehrend + }, + 'ReqdColltnDt': dayjs().add(3, "days").format("YYYY-MM-DDTHH:mm:ss"), + 'Cdtr': { + 'Nm': tenantData.name + }, + 'CdtrAcct': { + 'Id': { + 'IBAN': "DE" // TODO: IBAN Gläubiger EINSETZEN + } + }, + 'CdtrAgt': { + 'FinInstnId': { + 'BIC': "BIC" // TODO: BIC des Gläubigers einsetzen + } + }, + 'ChrgBr': "SLEV", + 'CdtrSchmeId': { + 'Id': { + 'PrvtId': { + 'Othr': { + 'Id': tenantData.creditorId, + 'SchmeNm': { + 'Prty': "SEPA" + } + } + } + } + }, + //TODO ITERATE ALL INVOICES HERE + 'DrctDbtTxInf': { + 'PmtId': { + 'EndToEndId': "" // TODO: EINDEUTIGE ReFERENZ wahrscheinlich RE Nummer + }, + 'InstdAmt': { + '@Ccy':"EUR", + '#text':100 //TODO: Rechnungssumme zwei NK mit Punkt + }, + 'DrctDbtTx': { + 'MndtRltdInf': { + 'MndtId': "", // TODO: Mandatsref, + 'DtOfSgntr': "", //TODO: Unterschrieben am, + 'AmdmntInd': "" //TODO: Mandat geändert + } + }, + 'DbtrAgt': { + 'FinInstnId': { + 'BIC': "", //TODO: BIC Debtor + } + }, + 'Dbtr': { + 'Nm': "" // TODO NAME Debtor + }, + 'DbtrAcct': { + 'Id': { + 'IBAN': "DE" // TODO IBAN Debtor + } + }, + 'RmtInf': { + 'Ustrd': "" //TODO Verwendungszweck Rechnungsnummer + } + } + } + } + + } + } + + + let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true}) + + console.log(doc.end({pretty:true})) + +} \ No newline at end of file diff --git a/src/utils/files.ts b/src/utils/files.ts new file mode 100644 index 0000000..0abe4cd --- /dev/null +++ b/src/utils/files.ts @@ -0,0 +1,95 @@ +import { PutObjectCommand } from "@aws-sdk/client-s3" +import { s3 } from "./s3" +import { secrets } from "./secrets" + +// Drizzle schema +import { files } from "../../db/schema" +import { eq } from "drizzle-orm" +import { FastifyInstance } from "fastify" + +export const saveFile = async ( + server: FastifyInstance, + tenant: number, + messageId: string | number | null, // Typ angepasst (oft null bei manueller Gen) + attachment: any, // Kann File, Buffer oder Mailparser-Objekt sein + folder: string | null, + type: string | null, + other: Record = {} +) => { + try { + // --------------------------------------------------- + // 1️⃣ FILE ENTRY ANLEGEN + // --------------------------------------------------- + const insertRes = await server.db + .insert(files) + .values({ + tenant, + folder, + type, + ...other + }) + .returning() + + const created = insertRes?.[0] + if (!created) { + console.error("File creation failed (no row returned)") + return null + } + + // Name ermitteln (Fallback Logik) + // Wenn attachment ein Buffer ist, muss der Name in 'other' stehen oder generiert werden + const filename = attachment.filename || other.filename || `${created.id}.pdf` + + // --------------------------------------------------- + // 2️⃣ BODY & CONTENT TYPE ERMITTELN + // --------------------------------------------------- + let body: Buffer | Uint8Array | string + let contentType = type || "application/octet-stream" + + if (Buffer.isBuffer(attachment)) { + // FALL 1: RAW BUFFER (von finishManualGeneration) + body = attachment + // ContentType wurde oben schon über 'type' Parameter gesetzt (z.B. application/pdf) + } else if (typeof File !== "undefined" && attachment instanceof File) { + // FALL 2: BROWSER FILE + body = Buffer.from(await attachment.arrayBuffer()) + contentType = attachment.type || contentType + } else if (attachment.content) { + // FALL 3: MAILPARSER OBJECT + body = attachment.content + contentType = attachment.contentType || contentType + } else { + console.error("saveFile: Unknown attachment format") + return null + } + + // --------------------------------------------------- + // 3️⃣ S3 UPLOAD + // --------------------------------------------------- + const key = `${tenant}/filesbyid/${created.id}/${filename}` + + await s3.send( + new PutObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: key, + Body: body, + ContentType: contentType, + ContentLength: body.length // <--- WICHTIG: Behebt den AWS Fehler + }) + ) + + // --------------------------------------------------- + // 4️⃣ PATH IN DB SETZEN + // --------------------------------------------------- + await server.db + .update(files) + .set({ path: key }) + .where(eq(files.id, created.id)) + + console.log(`File saved: ${key}`) + return { id: created.id, key } + } catch (err) { + console.error("saveFile error:", err) + return null + } +} \ No newline at end of file diff --git a/src/utils/functions.ts b/src/utils/functions.ts new file mode 100644 index 0000000..3b6dc7d --- /dev/null +++ b/src/utils/functions.ts @@ -0,0 +1,174 @@ +import {FastifyInstance} from "fastify"; +// import { PNG } from 'pngjs' +// import { ready as zplReady } from 'zpl-renderer-js' +// import { Utils } from '@mmote/niimbluelib' +// import { createCanvas } from 'canvas' +// import bwipjs from 'bwip-js' +// import Sharp from 'sharp' +// import fs from 'fs' + +import { tenants } from "../../db/schema" +import { eq } from "drizzle-orm" + +export const useNextNumberRangeNumber = async ( + server: FastifyInstance, + tenantId: number, + numberRange: string +) => { + // 1️⃣ Tenant laden + const [tenant] = await server.db + .select() + .from(tenants) + .where(eq(tenants.id, tenantId)) + + if (!tenant) { + throw new Error(`Tenant ${tenantId} not found`) + } + + const numberRanges = tenant.numberRanges || {} + + if (!numberRanges[numberRange]) { + throw new Error(`Number range '${numberRange}' not found`) + } + + const current = numberRanges[numberRange] + + // 2️⃣ Used Number generieren + const usedNumber = + (current.prefix || "") + + current.nextNumber + + (current.suffix || "") + + // 3️⃣ nextNumber erhöhen + const updatedRanges = { + // @ts-ignore + ...numberRanges, + [numberRange]: { + ...current, + nextNumber: current.nextNumber + 1 + } + } + + // 4️⃣ Tenant aktualisieren + await server.db + .update(tenants) + .set({ numberRanges: updatedRanges }) + .where(eq(tenants.id, tenantId)) + + return { usedNumber } +} + + +/* +export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') { + // 1️⃣ PNG dekodieren + const buffer = Buffer.from(base64Png, 'base64') + const png = PNG.sync.read(buffer) // liefert {width, height, data: Uint8Array(RGBA)} + + const { width, height, data } = png + console.log(width, height, data) + const cols = printDirection === 'left' ? height : width + const rows = printDirection === 'left' ? width : height + const rowsData = [] + + console.log(cols) + + if (cols % 8 !== 0) throw new Error('Column count must be multiple of 8') + + // 2️⃣ Zeilenweise durchgehen und Bits bilden + for (let row = 0; row < rows; row++) { + let isVoid = true + let blackPixelsCount = 0 + const rowData = new Uint8Array(cols / 8) + + for (let colOct = 0; colOct < cols / 8; colOct++) { + let pixelsOctet = 0 + for (let colBit = 0; colBit < 8; colBit++) { + const x = printDirection === 'left' ? row : colOct * 8 + colBit + const y = printDirection === 'left' ? height - 1 - (colOct * 8 + colBit) : row + const idx = (y * width + x) * 4 + const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2] + const isBlack = lum < 128 + if (isBlack) { + pixelsOctet |= 1 << (7 - colBit) + isVoid = false + blackPixelsCount++ + } + } + rowData[colOct] = pixelsOctet + } + + const newPart = { + dataType: isVoid ? 'void' : 'pixels', + rowNumber: row, + repeat: 1, + rowData: isVoid ? undefined : rowData, + blackPixelsCount, + } + + if (rowsData.length === 0) { + rowsData.push(newPart) + } else { + const last = rowsData[rowsData.length - 1] + let same = newPart.dataType === last.dataType + if (same && newPart.dataType === 'pixels') { + same = Utils.u8ArraysEqual(newPart.rowData, last.rowData) + } + if (same) last.repeat++ + else rowsData.push(newPart) + if (row % 200 === 199) { + rowsData.push({ + dataType: 'check', + rowNumber: row, + repeat: 0, + rowData: undefined, + blackPixelsCount: 0, + }) + } + } + } + + return { cols, rows, rowsData } +} + +export async function generateLabel(context,width,height) { + // Canvas für Hintergrund & Text + const canvas = createCanvas(width, height) + const ctx = canvas.getContext('2d') + + // Hintergrund weiß + ctx.fillStyle = '#FFFFFF' + ctx.fillRect(0, 0, width, height) + + // Überschrift + ctx.fillStyle = '#000000' + ctx.font = '32px Arial' + ctx.fillText(context.text, 20, 40) + + // 3) DataMatrix + const dataMatrixPng = await bwipjs.toBuffer({ + bcid: 'datamatrix', + text: context.datamatrix, + scale: 6, + }) + + // Basisbild aus Canvas + const base = await Sharp(canvas.toBuffer()) + .png() + .toBuffer() + + // Alles zusammen compositen + const final = await Sharp(base) + .composite([ + { input: dataMatrixPng, top: 60, left: 20 }, + ]) + .png() + .toBuffer() + + fs.writeFileSync('label.png', final) + + // Optional: Base64 zurückgeben (z.B. für API) + const base64 = final.toString('base64') + + return base64 +}*/ diff --git a/src/utils/gpt.ts b/src/utils/gpt.ts new file mode 100644 index 0000000..ec804ce --- /dev/null +++ b/src/utils/gpt.ts @@ -0,0 +1,204 @@ +import dayjs from "dayjs"; +import axios from "axios"; +import OpenAI from "openai"; +import { z } from "zod"; +import { zodResponseFormat } from "openai/helpers/zod"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import { Blob } from "buffer"; +import { FastifyInstance } from "fastify"; + +import { s3 } from "./s3"; +import { secrets } from "./secrets"; + +// Drizzle schema +import { vendors, accounts } from "../../db/schema"; +import {eq} from "drizzle-orm"; + +let openai: OpenAI | null = null; + +// --------------------------------------------------------- +// INITIALIZE OPENAI +// --------------------------------------------------------- +export const initOpenAi = async () => { + openai = new OpenAI({ + apiKey: secrets.OPENAI_API_KEY, + }); +}; + +// --------------------------------------------------------- +// STREAM → BUFFER +// --------------------------------------------------------- +async function streamToBuffer(stream: any): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream.on("data", (chunk: Buffer) => chunks.push(chunk)); + stream.on("error", reject); + stream.on("end", () => resolve(Buffer.concat(chunks))); + }); +} + +// --------------------------------------------------------- +// GPT RESPONSE FORMAT (Zod Schema) +// --------------------------------------------------------- +const InstructionFormat = z.object({ + invoice_number: z.string(), + invoice_date: z.string(), + invoice_duedate: z.string(), + invoice_type: z.string(), + delivery_type: z.string(), + delivery_note_number: z.string(), + reference: z.string(), + issuer: z.object({ + id: z.number().nullable().optional(), + name: z.string(), + address: z.string(), + phone: z.string(), + email: z.string(), + bank: z.string(), + bic: z.string(), + iban: z.string(), + }), + recipient: z.object({ + name: z.string(), + address: z.string(), + phone: z.string(), + email: z.string(), + }), + invoice_items: z.array( + z.object({ + description: z.string(), + unit: z.string(), + quantity: z.number(), + total: z.number(), + total_without_tax: z.number(), + tax_rate: z.number(), + ean: z.number().nullable().optional(), + article_number: z.number().nullable().optional(), + account_number: z.number().nullable().optional(), + account_id: z.number().nullable().optional(), + }) + ), + subtotal: z.number(), + tax_rate: z.number(), + tax: z.number(), + total: z.number(), + terms: z.string(), +}); + +// --------------------------------------------------------- +// MAIN FUNCTION – REPLACES SUPABASE VERSION +// --------------------------------------------------------- +export const getInvoiceDataFromGPT = async function ( + server: FastifyInstance, + file: any, + tenantId: number +) { + await initOpenAi(); + + if (!openai) { + throw new Error("OpenAI not initialized. Call initOpenAi() first."); + } + + console.log(`📄 Reading invoice file ${file.id}`); + + // --------------------------------------------------------- + // 1) DOWNLOAD PDF FROM S3 + // --------------------------------------------------------- + let fileData: Buffer; + + try { + const command = new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: file.path, + }); + + const response: any = await s3.send(command); + fileData = await streamToBuffer(response.Body); + } catch (err) { + console.log(`❌ S3 Download failed for file ${file.id}`, err); + return null; + } + + // Only process PDFs + if (!file.path.toLowerCase().endsWith(".pdf")) { + server.log.warn(`Skipping non-PDF file ${file.id}`); + return null; + } + + const fileBlob = new Blob([fileData], { type: "application/pdf" }); + + // --------------------------------------------------------- + // 2) SEND FILE TO PDF → TEXT API + // --------------------------------------------------------- + const form = new FormData(); + form.append("fileInput", fileBlob, file.path.split("/").pop()); + form.append("outputFormat", "txt"); + + let extractedText: string; + + try { + const res = await axios.post( + "http://23.88.52.85:8080/api/v1/convert/pdf/text", + form, + { + headers: { + "Content-Type": "multipart/form-data", + Authorization: `Bearer ${secrets.STIRLING_API_KEY}`, + }, + } + ); + + extractedText = res.data; + } catch (err) { + console.log("❌ PDF OCR API failed", err); + return null; + } + + // --------------------------------------------------------- + // 3) LOAD VENDORS + ACCOUNTS (DRIZZLE) + // --------------------------------------------------------- + const vendorList = await server.db + .select({ id: vendors.id, name: vendors.name }) + .from(vendors) + .where(eq(vendors.tenant,tenantId)); + + const accountList = await server.db + .select({ + id: accounts.id, + label: accounts.label, + number: accounts.number, + }) + .from(accounts); + + // --------------------------------------------------------- + // 4) GPT ANALYSIS + // --------------------------------------------------------- + + + + const completion = await openai.chat.completions.parse({ + model: "gpt-4o", + store: true, + response_format: zodResponseFormat(InstructionFormat as any, "instruction"), + messages: [ + { role: "user", content: extractedText }, + { + role: "user", + content: + "You extract structured invoice data.\n\n" + + `VENDORS: ${JSON.stringify(vendorList)}\n` + + `ACCOUNTS: ${JSON.stringify(accountList)}\n\n` + + "Match issuer by name to vendor.id.\n" + + "Match invoice items to account id based on label/number.\n" + + "Convert dates to YYYY-MM-DD.\n" + + "Keep invoice items in original order.\n", + }, + ], + }); + + const parsed = completion.choices[0].message.parsed; + + console.log(`🧾 Extracted invoice data for file ${file.id}`); + + return parsed; +}; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts new file mode 100644 index 0000000..1d11c5b --- /dev/null +++ b/src/utils/helpers.ts @@ -0,0 +1,106 @@ +// 🔧 Hilfsfunktionen +import { FastifyInstance } from "fastify" +import { eq, ilike, and } from "drizzle-orm" + +import { contacts, customers } from "../../db/schema" + +// ------------------------------------------------------------- +// Extract Domain +// ------------------------------------------------------------- +export function extractDomain(email: string) { + if (!email) return null + const parts = email.split("@") + return parts.length === 2 ? parts[1].toLowerCase() : null +} + +// ------------------------------------------------------------- +// Kunde oder Kontakt anhand E-Mail oder Domain finden +// ------------------------------------------------------------- +export async function findCustomerOrContactByEmailOrDomain( + server: FastifyInstance, + fromMail: string, + tenantId: number +) { + const sender = fromMail.toLowerCase() + const senderDomain = extractDomain(sender) + if (!senderDomain) return null + + // 1️⃣ Direkter Match über Contacts (email) + const contactMatch = await server.db + .select({ + id: contacts.id, + customer: contacts.customer, + }) + .from(contacts) + .where( + and( + eq(contacts.email, sender), + eq(contacts.tenant, tenantId) + ) + ) + .limit(1) + + if (contactMatch.length && contactMatch[0].customer) { + return { + customer: contactMatch[0].customer, + contact: contactMatch[0].id, + } + } + + // 2️⃣ Kunden anhand Domain vergleichen + const allCustomers = await server.db + .select({ + id: customers.id, + infoData: customers.infoData, + }) + .from(customers) + .where(eq(customers.tenant, tenantId)) + + for (const c of allCustomers) { + const info = c.infoData || {} + + // @ts-ignore + const email = info.email?.toLowerCase() + //@ts-ignore + const invoiceEmail = info.invoiceEmail?.toLowerCase() + const emailDomain = extractDomain(email) + const invoiceDomain = extractDomain(invoiceEmail) + + if ( + sender === email || + sender === invoiceEmail || + senderDomain === emailDomain || + senderDomain === invoiceDomain + ) { + return { customer: c.id, contact: null } + } + } + + return null +} + +// ------------------------------------------------------------- +// getNestedValue (für Sortierung & Suche im Backend) +// ------------------------------------------------------------- +export function getNestedValue(obj: any, path: string): any { + return path + .split(".") + .reduce((acc, part) => (acc && acc[part] !== undefined ? acc[part] : undefined), obj) +} + +// ------------------------------------------------------------- +// compareValues (Sortierung für paginated) +// ------------------------------------------------------------- +export function compareValues(a: any, b: any): number { + if (a === b) return 0 + if (a == null) return 1 + if (b == null) return -1 + + // String Compare + if (typeof a === "string" && typeof b === "string") { + return a.localeCompare(b) + } + + // Numerisch + return a < b ? -1 : 1 +} diff --git a/src/utils/history.ts b/src/utils/history.ts new file mode 100644 index 0000000..477c666 --- /dev/null +++ b/src/utils/history.ts @@ -0,0 +1,70 @@ +import { FastifyInstance } from "fastify" + +export async function insertHistoryItem( + server: FastifyInstance, + params: { + tenant_id: number + created_by: string | null + entity: string + entityId: string | number + action: "created" | "updated" | "unchanged" | "deleted" | "archived" + oldVal?: any + newVal?: any + text?: string + } +) { + const textMap = { + created: `Neuer Eintrag in ${params.entity} erstellt`, + updated: `Eintrag in ${params.entity} geändert`, + archived: `Eintrag in ${params.entity} archiviert`, + deleted: `Eintrag in ${params.entity} gelöscht` + } + + const columnMap: Record = { + customers: "customer", + vendors: "vendor", + projects: "project", + plants: "plant", + contacts: "contact", + inventoryitems: "inventoryitem", + products: "product", + profiles: "profile", + absencerequests: "absencerequest", + events: "event", + tasks: "task", + vehicles: "vehicle", + costcentres: "costcentre", + ownaccounts: "ownaccount", + documentboxes: "documentbox", + hourrates: "hourrate", + services: "service", + roles: "role", + checks: "check", + spaces: "space", + trackingtrips: "trackingtrip", + createddocuments: "createddocument", + inventoryitemgroups: "inventoryitemgroup", + bankstatements: "bankstatement" + } + + const fkColumn = columnMap[params.entity] + if (!fkColumn) { + server.log.warn(`Keine History-Spalte für Entity: ${params.entity}`) + return + } + + const entry = { + tenant: params.tenant_id, + created_by: params.created_by, + text: params.text || textMap[params.action], + action: params.action, + [fkColumn]: params.entityId, + oldVal: params.oldVal ? JSON.stringify(params.oldVal) : null, + newVal: params.newVal ? JSON.stringify(params.newVal) : null + } + + const { error } = await server.supabase.from("historyitems").insert([entry]) + if (error) { // @ts-ignore + console.log(error) + } +} diff --git a/src/utils/mailer.ts b/src/utils/mailer.ts new file mode 100644 index 0000000..ce52288 --- /dev/null +++ b/src/utils/mailer.ts @@ -0,0 +1,37 @@ +import nodemailer from "nodemailer" +import {secrets} from "./secrets" + +export let transporter = null +export const initMailer = async () => { + transporter = nodemailer.createTransport({ + host: secrets.MAILER_SMTP_HOST, + port: Number(secrets.MAILER_SMTP_PORT) || 587, + secure: secrets.MAILER_SMTP_SSL === "true", // true für 465, false für andere Ports + auth: { + user: secrets.MAILER_SMTP_USER, + pass: secrets.MAILER_SMTP_PASS, + }, + }) + console.log("Mailer Initialized!") +} + +export async function sendMail( + to: string, + subject: string, + html: string +): Promise<{ success: boolean; info?: any; error?: any }> { + try { + const info = await transporter.sendMail({ + from: secrets.MAILER_FROM, + to, + subject, + html, + }) + + // Nodemailer liefert eine Info-Response zurück + return { success: true, info } + } catch (err) { + console.error("❌ Fehler beim Mailversand:", err) + return { success: false, error: err } + } +} \ No newline at end of file diff --git a/src/utils/password.ts b/src/utils/password.ts new file mode 100644 index 0000000..11851ae --- /dev/null +++ b/src/utils/password.ts @@ -0,0 +1,15 @@ +import bcrypt from "bcrypt" + +export function generateRandomPassword(length = 12): string { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*" + let password = "" + for (let i = 0; i < length; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return password +} + +export async function hashPassword(password: string): Promise { + const saltRounds = 10 + return bcrypt.hash(password, saltRounds) +} \ No newline at end of file diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts new file mode 100644 index 0000000..a231a40 --- /dev/null +++ b/src/utils/pdf.ts @@ -0,0 +1,1126 @@ +import {PDFDocument, StandardFonts, rgb} from "pdf-lib" +import dayjs from "dayjs" +import {renderAsCurrency, splitStringBySpace} from "./stringRendering"; +import {FastifyInstance} from "fastify"; + +const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => { + /* + * @param x the wanted X Parameter in Millimeters from Top Left + * @param y the wanted Y Parameter in Millimeters from Top Left + * @param page the page Object + * + * @returns x,y object + * */ + + + let retX = x * 2.83 + let retY = page.getHeight()-(y*2.83) + + return { + x: retX, + y: retY + } +} + + +const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => { + + const {data:backgroundPDFData,error:backgroundPDFError} = await server.supabase.storage.from("files").download(path) + + return backgroundPDFData.arrayBuffer() +} + +const getDuration = (time) => { + const minutes = Math.floor(dayjs(time.stopped_at).diff(dayjs(time.started_at),'minutes',true)) + const hours = Math.floor(minutes/60) + return { + //dezimal: dez, + hours: hours, + minutes: minutes, + composed: `${hours}:${String(minutes % 60).padStart(2,"0")} Std` + } +} + + +export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => { + + const genPDF = async (invoiceData, backgroundSourceBuffer) => { + const pdfDoc = await PDFDocument.create() + + const font = await pdfDoc.embedFont(StandardFonts.Helvetica) + const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) + + let pages = [] + let pageCounter = 1 + + + //const backgroundPdfSourceBuffer = await fetch("/Briefpapier.pdf").then((res) => res.arrayBuffer()) + const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer) + + const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0]) + const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0]) + + // + const page1 = pdfDoc.addPage() + + // + + page1.drawPage(firstPageBackground, { + x: 0, + y: 0, + }) + // + pages.push(page1) + // + + + //Falzmarke 1 + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 105, page1), + end: getCoordinatesForPDFLib(5, 105, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Lochmarke + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 148.5, page1), + end: getCoordinatesForPDFLib(5, 148.5, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Falzmarke 2 + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 210, page1), + end: getCoordinatesForPDFLib(5, 210, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,45,page1), + end: getCoordinatesForPDFLib(105,45,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + if (!invoiceData.addressLine) console.log("Missing Addressline") + + pages[pageCounter - 1].drawText(invoiceData.adressLine, { + ...getCoordinatesForPDFLib(21, 48, page1), + size: 6, + color: rgb(0, 0, 0), + lineHeight: 6, + opacity: 1, + maxWidth: 240 + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,50,page1), + end: getCoordinatesForPDFLib(105,50,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + let partLinesAdded = 0 + invoiceData.recipient.forEach((info, index) => { + + + let maxSplitLength = 35 + let splittedContent = splitStringBySpace(info, maxSplitLength) + + + splittedContent.forEach((part, partIndex) => { + if (partIndex === 0) { + pages[pageCounter - 1].drawText(part, { + ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } else { + partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } + + /*if(partIndex > 0) partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + y: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5, page1).y, + x: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5,page1).x + 230 - font.widthOfTextAtSize(part,10), + size:10, + color:rgb(0,0,0), + lineHeight:10, + opacity: 1, + maxWidth: 240 + })*/ + + }) + }) + + //Rechts + + partLinesAdded = 0 + + invoiceData.info.forEach((info, index) => { + + let maxSplitLength = 34 + let splittedContent = splitStringBySpace(info.content, maxSplitLength) + + + splittedContent.forEach((part, partIndex) => { + if (partIndex === 0) { + pages[pageCounter - 1].drawText(info.label, { + ...getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } + + if (partIndex > 0) partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + y: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).y, + x: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).x + 230 - font.widthOfTextAtSize(part, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + }) + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(125,90,page1), + end: getCoordinatesForPDFLib(200,90,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + //Title + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,95,page1), + end: getCoordinatesForPDFLib(200,95,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + if (!invoiceData.title) console.log("Missing Title") + + pages[pageCounter - 1].drawText(invoiceData.title, { + ...getCoordinatesForPDFLib(20, 100, page1), + size: 13, + color: rgb(0, 0, 0), + lineHeight: 15, + opacity: 1, + maxWidth: 500 + }) + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,105,page1), + end: getCoordinatesForPDFLib(200,105,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + if (!invoiceData.description) console.log("Missing Description") + + if (invoiceData.description) { + pages[pageCounter - 1].drawText(invoiceData.description, { + ...getCoordinatesForPDFLib(20, 112, page1), + size: 13, + color: rgb(0, 0, 0), + lineHeight: 15, + opacity: 1, + maxWidth: 500 + }) + } + + if (!invoiceData.startText) console.log("Missing StartText") + + + pages[pageCounter - 1].drawText(invoiceData.startText, { + ...getCoordinatesForPDFLib(20, 119, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,115,page1), + end: getCoordinatesForPDFLib(200,115,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, 140, page1), + end: getCoordinatesForPDFLib(199, 140, page1), + thickness: 0.1, + color: rgb(0, 0, 0), + opacity: 1, + }) + + /*pages[pageCounter - 1].drawRectangle({ + ...getCoordinatesForPDFLib(20,140, page1), + width: 180 * 2.83, + height: 8 * 2.83, + color: rgb(0,0,0), + opacity: 0, + borderWidth: 0.1 + })*/ + + //Header + + pages[pageCounter - 1].drawText("Pos", { + ...getCoordinatesForPDFLib(21, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Menge", { + ...getCoordinatesForPDFLib(35, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Bezeichnung", { + ...getCoordinatesForPDFLib(52, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText("Steuer", { + ...getCoordinatesForPDFLib(135, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Einheitspreis", { + ...getCoordinatesForPDFLib(150, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Gesamt", { + y: getCoordinatesForPDFLib(25, 137, page1).y, + x: getCoordinatesForPDFLib(25, 137, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + + let rowHeight = 145.5 + + let pageIndex = 0 + + + invoiceData.rows.forEach((row, index) => { + + if (!["pagebreak", "title", "text"].includes(row.mode)) { + + + if (!row.pos) console.log("Missing Row Pos") + + pages[pageCounter - 1].drawText(String(row.pos), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + if (!row.quantity) console.log("Missing Row Quantity") + if (!row.unit) console.log("Missing Row Unit") + + pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.quantity} ${row.unit})` : `${row.quantity} ${row.unit}`, { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + let rowTextLines = 0 + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 35).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + font: fontBold + }) + rowTextLines = splitStringBySpace(row.text, 35).length + + } else { + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 80).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + font: fontBold + }) + + rowTextLines = splitStringBySpace(row.text, 80).length + } + + let rowDescriptionLines = 0 + + if (row.descriptionText) { + if (invoiceData.type !== "deliveryNotes") { + rowDescriptionLines = splitStringBySpace(row.descriptionText, 60).length + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 60).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + + } else { + rowDescriptionLines = splitStringBySpace(row.descriptionText, 80).length + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 80).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + } + } + + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText(`${row.taxPercent} %`, { + ...getCoordinatesForPDFLib(135, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + pages[pageCounter - 1].drawText(row.price, { + ...getCoordinatesForPDFLib(150, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + + pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, { + y: getCoordinatesForPDFLib(25, rowHeight, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight, page1).x + 490 - font.widthOfTextAtSize((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + + if (row.discountPercent > 0) { + + let text = row.discountText + + if (row.optional) text = `Optional - ${text}` + if (row.alternative) text = `Alternativ - ${text}` + + + pages[pageCounter - 1].drawText(text, { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize(text, 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } else if (row.optional) { + pages[pageCounter - 1].drawText("Optional", { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Optional", 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } else if (row.alternative) { + pages[pageCounter - 1].drawText("Alternativ", { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Alternativ", 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } + } + + if (row.descriptionText) { + rowHeight += rowDescriptionLines * 4.5 + rowTextLines * 5.5 + } else if (row.discountPercent) { + rowHeight += (rowTextLines + 1) * 5.5 + } else if (row.optional || row.alternative) { + rowHeight += (rowTextLines + 1) * 5.5 + } else { + rowHeight += rowTextLines * 5.5 + } + + + pageIndex += 1 + + + } else if (row.mode === 'pagebreak') { + + console.log(invoiceData.rows[index + 1]) + + if (invoiceData.rows[index + 1].mode === 'title') { + let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[invoiceData.rows[index + 1].pos - 2]]}` + pages[pageCounter - 1].drawText(transferSumText, { + y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + + const page = pdfDoc.addPage() + + page.drawPage(secondPageBackground, { + x: 0, + y: 0, + }) + + //Falzmarke 1 + page.drawLine({ + start: getCoordinatesForPDFLib(0, 105, page1), + end: getCoordinatesForPDFLib(7, 105, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Lochmarke + page.drawLine({ + start: getCoordinatesForPDFLib(0, 148.5, page1), + end: getCoordinatesForPDFLib(7, 148.5, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Falzmarke 2 + page.drawLine({ + start: getCoordinatesForPDFLib(0, 210, page1), + end: getCoordinatesForPDFLib(7, 210, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + page.drawText("Pos", { + ...getCoordinatesForPDFLib(21, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Menge", { + ...getCoordinatesForPDFLib(35, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Bezeichnung", { + ...getCoordinatesForPDFLib(52, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + if (invoiceData.type !== "deliveryNotes") { + page.drawText("Steuer", { + ...getCoordinatesForPDFLib(135, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Einheitspreis", { + ...getCoordinatesForPDFLib(150, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Gesamt", { + y: getCoordinatesForPDFLib(25, 22, page1).y, + x: getCoordinatesForPDFLib(25, 22, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + pageCounter += 1; + pageIndex = 0; + rowHeight = 30; + + pages.push(page) + + } else if (row.mode === 'title') { + if (index === 0 || pageIndex === 0) { + rowHeight += 3 + } else { + let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[row.pos - 2]]}` + pages[pageCounter - 1].drawText(transferSumText, { + y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(199, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + rowHeight += 5 + } + + pages[pageCounter - 1].drawText(String(row.pos), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 60).join("\n"), { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 500, + font: fontBold + }) + + rowHeight += splitStringBySpace(row.text, 60).length * 4.5 + } else if (row.mode === 'text') { + if (index === 0 || pageIndex === 0) { + rowHeight += 3 + } + + if (row.descriptionText) { + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 70).join("\n"), { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + + rowHeight += (splitStringBySpace(row.descriptionText, 70) || []).length * 4 + rowHeight += 4 + } + } + console.log(rowHeight) + }) + + + let endTextDiff = 35 + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(198, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + rowHeight += 6 + + + if (Object.keys(invoiceData.total.titleSums).length > 0) { + Object.keys(invoiceData.total.titleSums).forEach((key, index) => { + pages[pageCounter - 1].drawText(splitStringBySpace(key, 60).join("\n"), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + font: fontBold + }) + + pages[pageCounter - 1].drawText(invoiceData.total.titleSums[key], { + y: getCoordinatesForPDFLib(21, rowHeight, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight, page1).x + 500 - fontBold.widthOfTextAtSize(invoiceData.total.titleSums[key], 11), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + rowHeight += splitStringBySpace(key, 60).length * 5 + }) + + /*let titleSumsArray = Object.keys(invoiceData.total.titleSums) + titleSumsArray.forEach(sum => { + let length = splitStringBySpace(sum,60).length + rowHeight += length *6 + })*/ + + //rowHeight += Object.keys(invoiceData.total.titleSums) + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(198, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + + rowHeight += 5 + } + + invoiceData.totalArray.forEach((item, index) => { + pages[pageCounter - 1].drawText(item.label, { + ...getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText(item.content, { + y: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).x + 500 - fontBold.widthOfTextAtSize(item.content, 11), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + }) + + if (invoiceData.taxType !== "13b UStG" && invoiceData.taxType !== "19 UStG" && invoiceData.taxType !== "12.3 UStG") { + + } else { + if (invoiceData.taxType === "13b UStG") { + pages[pageCounter - 1].drawText("Die Umsatzsteuer für diese Leistung schuldet nach §13b UStG der Leistungsempfänger", { + ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } else if (invoiceData.taxType === "19 UStG") { + pages[pageCounter - 1].drawText("Als Kleinunternehmer im Sinne von § 19 Abs. 1 UStG wird keine Umsatzsteuer berechnet.", { + ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } else if (invoiceData.taxType === "12.3 UStG") { + pages[pageCounter - 1].drawText("Umsatzsteuer befreite Lieferung/Leistung für PV-Anlagen gemäß § 12 Absatz 3 UStG.", { + ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } + + + } + + + pages[pageCounter - 1].drawText(invoiceData.endText, { + ...getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + + return await pdfDoc.saveAsBase64() + } + + } + + const pdfBytes = await genPDF(invoiceData, await getBackgroundSourceBuffer(server,backgroundPath)) + + if(returnMode === "base64"){ + return { + mimeType: 'application/pdf', + base64: pdfBytes + } + } else { + return null + } +} + +export const createTimeSheetPDF = async (server: FastifyInstance, returnMode, data, backgroundPath: string) => { + + const genPDF = async (input, backgroundSourceBuffer) => { + const pdfDoc = await PDFDocument.create() + + const font = await pdfDoc.embedFont(StandardFonts.Helvetica) + const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) + + let pages = [] + let pageCounter = 1 + + const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer) + + const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0]) + // Fallback für einseitige Hintergründe + const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0]) + + const page1 = pdfDoc.addPage() + + page1.drawPage(firstPageBackground, { + x: 0, + y: 0, + }) + + pages.push(page1) + + console.log("PDF Input Data:", input) + + // --------------------------------------------------------- + // DATEN-EXTRAKTION MIT FALLBACKS (Calculated vs. Standard) + // --------------------------------------------------------- + + // Summen: Bevorzuge calculated..., falls vorhanden + const sumSubmitted = input.calculatedSumWorkingMinutesSubmitted ?? input.sumWorkingMinutesSubmitted ?? 0; + const sumApproved = input.calculatedSumWorkingMinutesApproved ?? input.sumWorkingMinutesApproved ?? 0; + + // Saldi: Bevorzuge calculated... + const saldoSubmitted = input.calculatedSaldoSubmitted ?? input.saldoSubmitted ?? input.saldoInOfficial ?? 0; + const saldoApproved = input.calculatedSaldoApproved ?? input.saldoApproved ?? input.saldo ?? 0; + + // Andere Summen (diese sind meist korrekt vom Backend) + const sumRecreation = input.sumWorkingMinutesRecreationDays ?? 0; + const sumVacation = input.sumWorkingMinutesVacationDays ?? 0; + const sumSick = input.sumWorkingMinutesSickDays ?? 0; + const sumTarget = input.timeSpanWorkingMinutes ?? 0; + + // Hilfsfunktion zur Formatierung von Minuten -> HH:MM + const fmtTime = (mins) => { + const m = Math.floor(Math.abs(mins)); + return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, "0")}`; + }; + + const fmtSaldo = (mins) => { + const sign = Math.sign(mins) >= 0 ? "+" : "-"; + return `${sign} ${fmtTime(mins)}`; + } + + + // --------------------------------------------------------- + // HEADER TEXTE ZEICHNEN + // --------------------------------------------------------- + + pages[pageCounter - 1].drawText(`Anwesenheitsauswertung`,{ + x: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).y, + size: 15, + font: fontBold + }) + + pages[pageCounter - 1].drawText(`Mitarbeiter: ${input.full_name || ''}`,{ + x: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).y, + size: 10, + }) + pages[pageCounter - 1].drawText(`Nummer: ${input.employee_number || '-'}`,{ + x: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).y, + size: 10, + }) + + // Zeile 1: Eingereicht & Genehmigt + pages[pageCounter - 1].drawText(`Eingereicht: ${fmtTime(sumSubmitted)} Std`,{ + x: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).y, + size: 10, + }) + pages[pageCounter - 1].drawText(`Genehmigt: ${fmtTime(sumApproved)} Std`,{ + x: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).y, + size: 10, + }) + + // Zeile 2: Ausgleichstage + pages[pageCounter - 1].drawText(`Feiertagsausgleich: ${fmtTime(sumRecreation)} Std`,{ + x: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).y, + size: 10, + }) + pages[pageCounter - 1].drawText(`Urlaubsausgleich: ${fmtTime(sumVacation)} Std`,{ + x: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).y, + size: 10, + }) + pages[pageCounter - 1].drawText(`Krankheitsausgleich: ${fmtTime(sumSick)} Std`,{ + x: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).y, + size: 10, + }) + + // Zeile 3: Soll & Saldo + pages[pageCounter - 1].drawText(`Soll Stunden: ${fmtTime(sumTarget)} Std`,{ + x: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).y, + size: 10, + }) + + // Wir nutzen hier die Begriffe "Inoffiziell" (Submitted Saldo) und "Saldo" (Approved Saldo) + pages[pageCounter - 1].drawText(`Inoffizielles Saldo: ${fmtSaldo(saldoSubmitted)} Std`,{ + x: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).y, + size: 10, + }) + pages[pageCounter - 1].drawText(`Saldo: ${fmtSaldo(saldoApproved)} Std`,{ + x: getCoordinatesForPDFLib(20,115,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,115,pages[pageCounter -1]).y, + size: 10, + }) + + // Tabellen-Header + pages[pageCounter - 1].drawText(`Start:`,{ + x: getCoordinatesForPDFLib(20,125,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,125,pages[pageCounter -1]).y, + size: 10, + }) + + pages[pageCounter - 1].drawText(`Ende:`,{ + x: getCoordinatesForPDFLib(60,125,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(60,125,pages[pageCounter -1]).y, + size: 10, + }) + + pages[pageCounter - 1].drawText(`Dauer:`,{ + x: getCoordinatesForPDFLib(100,125,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(100,125,pages[pageCounter -1]).y, + size: 10, + }) + + // --------------------------------------------------------- + // TABELLE GENERIEREN (Spans verarbeiten) + // --------------------------------------------------------- + + let rowHeight = 130 + + // WICHTIG: input.spans verwenden, fallback auf input.times (altes Format) + // Wir filtern leere Einträge raus + const rawItems = (input.spans || input.times || []).filter(t => t); + + // Sortierung umkehren (neueste zuletzt für den Druck? Oder wie gewünscht) + // Im Original war es .reverse(). + let reversedInput = rawItems.slice().reverse(); + + let splitted = [] + const splittedLength = Math.floor((reversedInput.length - 25) / 40) + + // Erste Seite hat weniger Platz wegen Header (25 Zeilen) + splitted.push(reversedInput.slice(0,25)) + + let lastIndex = 25 + for (let i = 0; i < splittedLength; ++i ) { + splitted.push(reversedInput.slice(lastIndex, lastIndex + (i + 1) * 40)) + lastIndex = lastIndex + (i + 1) * 40 + 1 + } + + if(reversedInput.slice(lastIndex, reversedInput.length).length > 0) { + splitted.push(reversedInput.slice(lastIndex, reversedInput.length)) + } + + console.log("PDF Pages Chunks:", splitted.length) + + splitted.forEach((chunk,index) => { + if(index > 0) { + const page = pdfDoc.addPage() + + page.drawPage(secondPageBackground, { + x: 0, + y: 0, + }) + + pages.push(page) + pageCounter++ + rowHeight = 20 + } + + chunk.forEach(span => { + // Mapping für Felder: spans nutzen 'startedAt', times nutzten 'started_at' + const startStr = span.startedAt || span.started_at; + const endStr = span.endedAt || span.stopped_at; // endedAt oder stopped_at + + // Dauer berechnen (da Spans keine duration_minutes haben) + let durationStr = ""; + if (startStr && endStr) { + const diffMins = dayjs(endStr).diff(dayjs(startStr), 'minute'); + durationStr = fmtTime(diffMins); + } else if (span.duration_minutes) { + durationStr = fmtTime(span.duration_minutes); + } else if (span.duration) { // Falls schon formatiert übergeben + durationStr = span.duration; + } + + pages[pageCounter - 1].drawText(`${dayjs(startStr).format("HH:mm DD.MM.YY")}`,{ + x: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).y, + size: 10, + }) + + pages[pageCounter - 1].drawText(`${endStr ? dayjs(endStr).format("HH:mm DD.MM.YY") : 'läuft...'}`,{ + x: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).y, + size: 10, + }) + + pages[pageCounter - 1].drawText(`${durationStr}`,{ + x: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).y, + size: 10, + }) + + // Optional: Status anzeigen? + /*pages[pageCounter - 1].drawText(`${span.status || span.state || ''}`,{ + x: getCoordinatesForPDFLib(130,rowHeight,pages[pageCounter -1]).x, + y: getCoordinatesForPDFLib(130,rowHeight,pages[pageCounter -1]).y, + size: 8, + })*/ + + rowHeight += 6 + }) + }) + + return await pdfDoc.saveAsBase64() + } + + try { + const pdfBytes = await genPDF(data, await getBackgroundSourceBuffer(server,backgroundPath)) + + if(returnMode === "base64"){ + return { + mimeType: 'application/pdf', + base64: pdfBytes + } + } else { + return "test" + } + } catch(error) { + console.log(error) + throw error; // Fehler weiterwerfen, damit er oben ankommt + } +} \ No newline at end of file diff --git a/src/utils/resource.config.ts b/src/utils/resource.config.ts new file mode 100644 index 0000000..99c9713 --- /dev/null +++ b/src/utils/resource.config.ts @@ -0,0 +1,174 @@ +import { + accounts, + bankaccounts, + bankrequisitions, + bankstatements, + contacts, + contracts, + costcentres, + createddocuments, + customers, + files, + filetags, + folders, + hourrates, + incominginvoices, + inventoryitemgroups, + inventoryitems, + letterheads, + ownaccounts, + plants, + productcategories, + products, + projects, + projecttypes, + serialExecutions, + servicecategories, + services, + spaces, + statementallocations, + tasks, + texttemplates, + units, + vehicles, + vendors +} from "../../db/schema"; + +export const resourceConfig = { + projects: { + searchColumns: ["name"], + mtoLoad: ["customer","plant","contract","projecttype"], + mtmLoad: ["tasks", "files","createddocuments"], + table: projects, + numberRangeHolder: "projectNumber" + }, + customers: { + searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"], + mtmLoad: ["contacts","projects","plants","createddocuments","contracts"], + table: customers, + numberRangeHolder: "customerNumber", + }, + contacts: { + searchColumns: ["firstName", "lastName", "email", "phone", "notes"], + table: contacts, + mtoLoad: ["customer","vendor"] + }, + contracts: { + table: contracts, + searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"], + numberRangeHolder: "contractNumber", + }, + plants: { + table: plants, + mtoLoad: ["customer"], + mtmLoad: ["projects","tasks","files"], + }, + projecttypes: { + table: projecttypes + }, + vendors: { + table: vendors, + searchColumns: ["name","vendorNumber","notes","defaultPaymentType"], + numberRangeHolder: "vendorNumber", + }, + files: { + table: files + }, + folders: { + table: folders + }, + filetags: { + table: filetags + }, + inventoryitems: { + table: inventoryitems, + numberRangeHolder: "articleNumber", + }, + inventoryitemgroups: { + table: inventoryitemgroups + }, + products: { + table: products, + searchColumns: ["name","manufacturer","ean","barcode","description","manfacturer_number","article_number"], + }, + productcategories: { + table: productcategories + }, + services: { + table: services, + mtoLoad: ["unit"], + searchColumns: ["name","description"], + }, + servicecategories: { + table: servicecategories + }, + units: { + table: units, + }, + vehicles: { + table: vehicles, + searchColumns: ["name","license_plate","vin","color"], + }, + hourrates: { + table: hourrates, + searchColumns: ["name"], + }, + spaces: { + table: spaces, + searchColumns: ["name","space_number","type","info_data"], + numberRangeHolder: "spaceNumber", + }, + ownaccounts: { + table: ownaccounts, + searchColumns: ["name","description","number"], + }, + costcentres: { + table: costcentres, + searchColumns: ["name","number","description"], + mtoLoad: ["vehicle","project","inventoryitem"], + numberRangeHolder: "number", + }, + tasks: { + table: tasks, + }, + letterheads: { + table: letterheads, + + }, + createddocuments: { + table: createddocuments, + mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"], + mtmLoad: ["statementallocations","files","createddocuments"], + mtmListLoad: ["statementallocations"], + }, + texttemplates: { + table: texttemplates + }, + incominginvoices: { + table: incominginvoices, + mtmLoad: ["statementallocations","files"], + mtmListLoad: ["statementallocations"], + mtoLoad: ["vendor"], + }, + statementallocations: { + table: statementallocations, + mtoLoad: ["customer","vendor","incominginvoice","createddocument","ownaccount","bankstatement"] + }, + accounts: { + table: accounts, + }, + bankstatements: { + table: bankstatements, + mtmListLoad: ["statementallocations"], + mtmLoad: ["statementallocations"], + }, + bankaccounts: { + table: bankaccounts, + }, + bankrequisitions: { + table: bankrequisitions, + }, + serialexecutions: { + table: serialExecutions + } +} \ No newline at end of file diff --git a/src/utils/s3.ts b/src/utils/s3.ts new file mode 100644 index 0000000..7a33d1c --- /dev/null +++ b/src/utils/s3.ts @@ -0,0 +1,18 @@ +import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3" +import {secrets} from "./secrets"; + + + +export let s3 = null + +export const initS3 = async () => { + s3 = new S3Client({ + endpoint: secrets.S3_ENDPOINT, // z. B. http://localhost:9000 für MinIO + region: secrets.S3_REGION, + credentials: { + accessKeyId: secrets.S3_ACCESS_KEY, + secretAccessKey: secrets.S3_SECRET_KEY, + }, + forcePathStyle: true, // wichtig für MinIO + }) +} diff --git a/src/utils/secrets.ts b/src/utils/secrets.ts new file mode 100644 index 0000000..c21e2d7 --- /dev/null +++ b/src/utils/secrets.ts @@ -0,0 +1,63 @@ +import {InfisicalSDK} from "@infisical/sdk" + +const client = new InfisicalSDK({ + siteUrl: "https://secrets.fedeo.io" +}) + + + +export let secrets = { + +} as { + COOKIE_SECRET: string + JWT_SECRET: string + PORT: number + HOST: string + DATABASE_URL: string + SUPABASE_URL: string + SUPABASE_SERVICE_ROLE_KEY: string + S3_BUCKET: string + ENCRYPTION_KEY: string + MAILER_SMTP_HOST: string + MAILER_SMTP_PORT: number + MAILER_SMTP_SSL: string + MAILER_SMTP_USER: string + MAILER_SMTP_PASS: string + MAILER_FROM: string + S3_ENDPOINT: string + S3_REGION: string + S3_ACCESS_KEY: string + S3_SECRET_KEY: string + M2M_API_KEY: string + API_BASE_URL: string + GOCARDLESS_BASE_URL: string + GOCARDLESS_SECRET_ID: string + GOCARDLESS_SECRET_KEY: string + DOKUBOX_IMAP_HOST: string + DOKUBOX_IMAP_PORT: number + DOKUBOX_IMAP_SECURE: boolean + DOKUBOX_IMAP_USER: string + DOKUBOX_IMAP_PASSWORD: string + OPENAI_API_KEY: string + STIRLING_API_KEY: string +} + +export async function loadSecrets () { + + await client.auth().universalAuth.login({ + clientId: process.env.INFISICAL_CLIENT_ID, + clientSecret: process.env.INFISICAL_CLIENT_SECRET, + }); + + const allSecrets = await client.secrets().listSecrets({ + environment: "dev", // stg, dev, prod, or custom environment slugs + projectId: "39774094-2aaf-49fb-a213-d6b2c10f6144" + }); + + allSecrets.secrets.forEach(secret => { + secrets[secret.secretKey] = secret.secretValue + }) + console.log("✅ Secrets aus Infisical geladen"); + console.log(Object.keys(secrets).length + " Stück") +} + diff --git a/src/utils/sort.ts b/src/utils/sort.ts new file mode 100644 index 0000000..4cb28ef --- /dev/null +++ b/src/utils/sort.ts @@ -0,0 +1,40 @@ +/** + * Sortiert ein Array von Objekten anhand einer Spalte. + * + * @param data Array von Objekten + * @param column Sortierspalte (Property-Name im Objekt) + * @param ascending true = aufsteigend, false = absteigend + */ +export function sortData>( + data: T[], + column?: keyof T | null, + ascending: boolean = true +): T[] { + if (!column) return data + + return [...data].sort((a, b) => { + const valA = a[column] + const valB = b[column] + + // null/undefined nach hinten + if (valA == null && valB != null) return 1 + if (valB == null && valA != null) return -1 + if (valA == null && valB == null) return 0 + + // Zahlenvergleich + if (typeof valA === "number" && typeof valB === "number") { + return ascending ? valA - valB : valB - valA + } + + // Datumsvergleich + // @ts-ignore + if (valA instanceof Date && valB instanceof Date) { + return ascending ? valA.getTime() - valB.getTime() : valB.getTime() - valA.getTime() + } + + // Fallback: Stringvergleich + return ascending + ? String(valA).localeCompare(String(valB)) + : String(valB).localeCompare(String(valA)) + }) +} \ No newline at end of file diff --git a/src/utils/stringRendering.ts b/src/utils/stringRendering.ts new file mode 100644 index 0000000..72dbad1 --- /dev/null +++ b/src/utils/stringRendering.ts @@ -0,0 +1,51 @@ + +export const renderAsCurrency = (value: string | number,currencyString = "€") => { + return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}` +} + +export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => { + + if(removeLinebreaks) { + input = input.replaceAll("\n","") + } + + let splitStrings: string[] = [] + + input.split("\n").forEach(string => { + splitStrings.push(string) + }) + + let returnSplitStrings: string[] = [] + + splitStrings.forEach(string => { + let regex = / /gi, result, indices = []; + while ( (result = regex.exec(string)) ) { + indices.push(result.index); + } + + let lastIndex = 0 + + if(string.length > maxSplitLength) { + let tempStrings = [] + + for (let i = maxSplitLength; i < string.length; i = i + maxSplitLength) { + let nearestIndex = indices.length > 0 ? indices.reduce(function(prev, curr) { + return (Math.abs(curr - i) < Math.abs(prev - i) ? curr : prev); + }) : i + + tempStrings.push(string.substring(lastIndex,nearestIndex)) + + lastIndex = indices.length > 0 ? nearestIndex + 1 : nearestIndex + } + + tempStrings.push(string.substring(lastIndex,input.length)) + + returnSplitStrings.push(...tempStrings) + + } else { + returnSplitStrings.push(string) + } + }) + + return returnSplitStrings +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..29f598d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021"], + "module": "commonjs", + "outDir": "dist", + "strict": false, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmitOnError": false, + "forceConsistentCasingInFileNames": true, + "rootDir": "." + }, + "include": ["src","db","*.ts"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file