Compare commits
4 Commits
81ce9d263d
...
bace26c084
| Author | SHA1 | Date | |
|---|---|---|---|
| bace26c084 | |||
| 274f3d5795 | |||
| 168d2fce6e | |||
| 6dcd8b1863 |
32
.env.example
32
.env.example
@@ -64,13 +64,11 @@ FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||
|
||||
# FEDEO Matrix-Kommunikation
|
||||
#
|
||||
# Diese Werte werden von docker-compose.yml gelesen, wenn das Profil "matrix"
|
||||
# genutzt wird. Für produktive Systeme müssen alle Geheimnisse ersetzt werden.
|
||||
# Diese Werte werden von docker-compose.yml und docker-compose.selfhost.yml
|
||||
# gelesen, wenn das Profil "matrix" genutzt wird. Für produktive Systeme
|
||||
# müssen alle Geheimnisse ersetzt werden.
|
||||
|
||||
MATRIX_SERVER_NAME=fedeo.de
|
||||
MATRIX_HOMESERVER_HOST=matrix.fedeo.de
|
||||
MATRIX_RTC_HOST=call.fedeo.de
|
||||
MATRIX_TURN_HOST=turn.fedeo.de
|
||||
MATRIX_SERVER_NAME=app.example.com
|
||||
|
||||
MATRIX_POSTGRES_DB=synapse
|
||||
MATRIX_POSTGRES_USER=synapse
|
||||
@@ -81,6 +79,15 @@ MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
||||
LIVEKIT_KEY=fedeo-livekit
|
||||
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
||||
|
||||
# Backend-Integration im Selfhost-Stack
|
||||
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
||||
MATRIX_RTC_HOST=app.example.com
|
||||
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
||||
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
||||
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-synapse-homeserver-yaml
|
||||
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
||||
|
||||
# Lokale Matrix-Entwicklung
|
||||
MATRIX_DEV_SYNAPSE_PORT=8008
|
||||
MATRIX_DEV_ELEMENT_PORT=8080
|
||||
@@ -94,10 +101,9 @@ MATRIX_DEV_TURN_PORT=3478
|
||||
MATRIX_DEV_TURN_MIN_PORT=49160
|
||||
MATRIX_DEV_TURN_MAX_PORT=49200
|
||||
|
||||
# Backend-Integration gegen den lokalen Matrix-Stack
|
||||
MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||
MATRIX_RTC_JWT_URL=http://localhost:8081
|
||||
MATRIX_LIVEKIT_URL=ws://localhost:7880
|
||||
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
||||
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080
|
||||
# Lokale Backend-Integration gegen den Matrix-Entwicklungsstack
|
||||
# MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||
# MATRIX_RTC_JWT_URL=http://localhost:8081
|
||||
# MATRIX_LIVEKIT_URL=ws://localhost:7880
|
||||
# MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
||||
# NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080
|
||||
|
||||
38
README.md
38
README.md
@@ -191,14 +191,31 @@ FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||
|
||||
MATRIX_SERVER_NAME=app.example.com
|
||||
MATRIX_POSTGRES_DB=synapse
|
||||
MATRIX_POSTGRES_USER=synapse
|
||||
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
|
||||
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
||||
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
||||
MATRIX_RTC_HOST=app.example.com
|
||||
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
||||
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
||||
MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-synapse-homeserver-yaml
|
||||
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||
LIVEKIT_KEY=fedeo-livekit
|
||||
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
||||
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
||||
```
|
||||
|
||||
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
|
||||
|
||||
## Docker Compose mit optionaler S3-MinIO-Option
|
||||
## Docker Compose mit optionalem S3 und Matrix
|
||||
|
||||
Die Selfhost-Konfiguration liegt in `docker-compose.selfhost.yml`. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
|
||||
|
||||
Der Matrix-Stack ist im Selfhost-Compose als optionales Profil `matrix` enthalten. Er umfasst Synapse, eine eigene PostgreSQL-Datenbank für Synapse, Redis, `.well-known/matrix`, coturn, LiveKit, den LiveKit-JWT-Service und Element Web. Das einfache Selfhost-Setup nutzt nur `DOMAIN`: Synapse läuft unter `https://DOMAIN/_matrix`, Matrix-Well-Known unter `https://DOMAIN/.well-known/matrix`, LiveKit unter `https://DOMAIN/livekit/sfu`, der JWT-Service unter `https://DOMAIN/livekit/jwt` und Element Web unter `https://DOMAIN/element`. Vor dem ersten Start musst du `matrix/synapse/homeserver.yaml` erzeugen und `matrix/selfhost/element-config.json` auf deine Domain anpassen.
|
||||
|
||||
Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest.
|
||||
|
||||
```yaml
|
||||
@@ -399,6 +416,23 @@ docker compose -f docker-compose.selfhost.yml build
|
||||
docker compose -f docker-compose.selfhost.yml up -d
|
||||
```
|
||||
|
||||
Mit Matrix-Profil:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.selfhost.yml --profile matrix up -d
|
||||
```
|
||||
|
||||
Synapse-Konfiguration vor dem ersten Matrix-Start erzeugen:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.selfhost.yml --profile matrix run --rm \
|
||||
-e SYNAPSE_SERVER_NAME="${MATRIX_SERVER_NAME}" \
|
||||
-e SYNAPSE_REPORT_STATS=no \
|
||||
matrix-synapse generate
|
||||
```
|
||||
|
||||
Danach in `matrix/synapse/homeserver.yaml` mindestens Datenbank, Redis, `public_baseurl`, TURN und `registration_shared_secret` setzen. Der Wert von `registration_shared_secret` muss zusätzlich als `MATRIX_REGISTRATION_SHARED_SECRET` in die `.env`, damit FEDEO Matrix-Nutzer provisionieren kann.
|
||||
|
||||
Danach Status prufen:
|
||||
|
||||
```bash
|
||||
@@ -449,6 +483,8 @@ Regelmassig sichern:
|
||||
|
||||
- `./postgres`
|
||||
- `./minio` falls MinIO lokal genutzt wird
|
||||
- `./matrix/postgres` falls Matrix lokal betrieben wird
|
||||
- `./matrix/synapse` falls Matrix lokal betrieben wird
|
||||
- `./traefik/letsencrypt/acme.json`
|
||||
- deine `.env`
|
||||
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
|
||||
|
||||
58
backend/db/migrations/0043_communication_rooms.sql
Normal file
58
backend/db/migrations/0043_communication_rooms.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
CREATE TABLE IF NOT EXISTS "communication_rooms" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"tenant_id" bigint NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"topic" text,
|
||||
"type" text DEFAULT 'room' NOT NULL,
|
||||
"entity_type" text,
|
||||
"entity_id" bigint,
|
||||
"entity_uuid" uuid,
|
||||
"matrix_room_id" text,
|
||||
"matrix_alias" text,
|
||||
"parent_space_room_id" text,
|
||||
"archived" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"created_by" uuid,
|
||||
"updated_by" uuid
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_tenant_id_tenants_id_fk'
|
||||
) THEN
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
|
||||
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||
ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_created_by_auth_users_id_fk'
|
||||
) THEN
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
|
||||
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||
ON DELETE no action ON UPDATE no action;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_updated_by_auth_users_id_fk'
|
||||
) THEN
|
||||
ALTER TABLE "communication_rooms"
|
||||
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
|
||||
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||
ON DELETE no action ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "communication_rooms_tenant_key_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id", "key");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "communication_rooms_tenant_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "communication_rooms_entity_idx"
|
||||
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");
|
||||
@@ -302,6 +302,13 @@
|
||||
"when": 1780153200000,
|
||||
"tag": "0042_profile_availability_note",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 43,
|
||||
"version": "7",
|
||||
"when": 1780156800000,
|
||||
"tag": "0043_communication_rooms",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -130,6 +130,15 @@ services:
|
||||
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME: ${FEDEO_BOOTSTRAP_ADMIN_LAST_NAME:-Benutzer}
|
||||
FEDEO_BOOTSTRAP_TENANT_NAME: ${FEDEO_BOOTSTRAP_TENANT_NAME:-FEDEO}
|
||||
FEDEO_BOOTSTRAP_TENANT_SHORT: ${FEDEO_BOOTSTRAP_TENANT_SHORT:-FEDEO}
|
||||
MATRIX_HOMESERVER_URL: ${MATRIX_HOMESERVER_URL:-http://matrix-synapse:8008}
|
||||
MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
||||
MATRIX_RTC_HOST: ${MATRIX_RTC_HOST:-${DOMAIN}}
|
||||
MATRIX_RTC_JWT_URL: ${MATRIX_RTC_JWT_URL:-}
|
||||
MATRIX_LIVEKIT_URL: ${MATRIX_LIVEKIT_URL:-}
|
||||
MATRIX_REGISTRATION_SHARED_SECRET: ${MATRIX_REGISTRATION_SHARED_SECRET:-}
|
||||
MATRIX_SERVICE_USER_LOCALPART: ${MATRIX_SERVICE_USER_LOCALPART:-fedeo_service}
|
||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-}
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
|
||||
@@ -165,6 +174,201 @@ services:
|
||||
networks:
|
||||
- web
|
||||
|
||||
matrix-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: fedeo-matrix-db
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
environment:
|
||||
POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
||||
POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
||||
POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD:-change-this-matrix-db-password}
|
||||
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate=C --lc-ctype=C
|
||||
volumes:
|
||||
- ./matrix/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${MATRIX_POSTGRES_USER:-synapse} -d ${MATRIX_POSTGRES_DB:-synapse}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- internal
|
||||
|
||||
matrix-redis:
|
||||
image: redis:7-alpine
|
||||
container_name: fedeo-matrix-redis
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
networks:
|
||||
- internal
|
||||
|
||||
matrix-synapse:
|
||||
image: ghcr.io/element-hq/synapse:latest
|
||||
container_name: fedeo-matrix-synapse
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
depends_on:
|
||||
matrix-db:
|
||||
condition: service_healthy
|
||||
matrix-redis:
|
||||
condition: service_started
|
||||
environment:
|
||||
SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
|
||||
volumes:
|
||||
- ./matrix/synapse:/data
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.fedeo-matrix.rule=Host(`${DOMAIN}`) && PathPrefix(`/_matrix`)
|
||||
- traefik.http.routers.fedeo-matrix.entrypoints=websecure
|
||||
- traefik.http.routers.fedeo-matrix.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.fedeo-matrix.loadbalancer.server.port=8008
|
||||
- traefik.docker.network=fedeo_web
|
||||
networks:
|
||||
- web
|
||||
- internal
|
||||
|
||||
matrix-well-known:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: fedeo-matrix-well-known
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
volumes:
|
||||
- ./matrix/well-known:/usr/share/nginx/html/.well-known/matrix:ro
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolalloworiginlist=*
|
||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowmethods=GET,OPTIONS
|
||||
- traefik.http.middlewares.fedeo-matrix-well-known-cors.headers.accesscontrolallowheaders=Content-Type,Authorization
|
||||
- traefik.http.routers.fedeo-matrix-well-known.rule=Host(`${DOMAIN}`) && PathPrefix(`/.well-known/matrix`)
|
||||
- traefik.http.routers.fedeo-matrix-well-known.entrypoints=websecure
|
||||
- traefik.http.routers.fedeo-matrix-well-known.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.fedeo-matrix-well-known.middlewares=fedeo-matrix-well-known-cors
|
||||
- traefik.http.services.fedeo-matrix-well-known.loadbalancer.server.port=80
|
||||
- traefik.docker.network=fedeo_web
|
||||
networks:
|
||||
- web
|
||||
|
||||
matrix-turn:
|
||||
image: instrumentisto/coturn:4
|
||||
container_name: fedeo-matrix-turn
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
command:
|
||||
- --fingerprint
|
||||
- --use-auth-secret
|
||||
- --static-auth-secret=${MATRIX_TURN_SHARED_SECRET:-change-this-turn-secret}
|
||||
- --realm=${MATRIX_SERVER_NAME:-${DOMAIN}}
|
||||
- --listening-port=3478
|
||||
- --tls-listening-port=5349
|
||||
- --min-port=49160
|
||||
- --max-port=49200
|
||||
- --no-cli
|
||||
- --no-tlsv1
|
||||
- --no-tlsv1_1
|
||||
ports:
|
||||
- "3478:3478/tcp"
|
||||
- "3478:3478/udp"
|
||||
- "5349:5349/tcp"
|
||||
- "49160-49200:49160-49200/udp"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
matrix-livekit:
|
||||
image: livekit/livekit-server:v1.9
|
||||
container_name: fedeo-matrix-livekit
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
depends_on:
|
||||
- matrix-redis
|
||||
entrypoint: /bin/sh
|
||||
command:
|
||||
- -ec
|
||||
- |
|
||||
cat >/tmp/livekit.yaml <<EOF
|
||||
port: 7880
|
||||
redis:
|
||||
address: matrix-redis:6379
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
port_range_start: 50000
|
||||
port_range_end: 50100
|
||||
use_external_ip: true
|
||||
keys:
|
||||
${LIVEKIT_KEY:-fedeo-livekit}: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
||||
room:
|
||||
auto_create: true
|
||||
EOF
|
||||
exec /livekit-server --config /tmp/livekit.yaml
|
||||
ports:
|
||||
- "7881:7881/tcp"
|
||||
- "50000-50100:50000-50100/udp"
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.middlewares.fedeo-matrix-livekit-strip.stripprefix.prefixes=/livekit/sfu
|
||||
- traefik.http.routers.fedeo-matrix-livekit.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit/sfu`)
|
||||
- traefik.http.routers.fedeo-matrix-livekit.entrypoints=websecure
|
||||
- traefik.http.routers.fedeo-matrix-livekit.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.fedeo-matrix-livekit.middlewares=fedeo-matrix-livekit-strip
|
||||
- traefik.http.services.fedeo-matrix-livekit.loadbalancer.server.port=7880
|
||||
- traefik.docker.network=fedeo_web
|
||||
networks:
|
||||
- web
|
||||
- internal
|
||||
|
||||
matrix-rtc-jwt:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
||||
container_name: fedeo-matrix-rtc-jwt
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
depends_on:
|
||||
- matrix-livekit
|
||||
- matrix-synapse
|
||||
environment:
|
||||
LIVEKIT_URL: wss://${DOMAIN}/livekit/sfu
|
||||
LIVEKIT_KEY: ${LIVEKIT_KEY:-fedeo-livekit}
|
||||
LIVEKIT_SECRET: ${LIVEKIT_SECRET:-change-this-livekit-secret-please-replace}
|
||||
LIVEKIT_FULL_ACCESS_HOMESERVERS: ${MATRIX_SERVER_NAME:-${DOMAIN}}
|
||||
LIVEKIT_JWT_BIND: :8080
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.middlewares.fedeo-matrix-rtc-jwt-strip.stripprefix.prefixes=/livekit/jwt
|
||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.rule=Host(`${DOMAIN}`) && PathPrefix(`/livekit/jwt`)
|
||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.entrypoints=websecure
|
||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.fedeo-matrix-rtc-jwt.middlewares=fedeo-matrix-rtc-jwt-strip
|
||||
- traefik.http.services.fedeo-matrix-rtc-jwt.loadbalancer.server.port=8080
|
||||
- traefik.docker.network=fedeo_web
|
||||
networks:
|
||||
- web
|
||||
- internal
|
||||
|
||||
matrix-element:
|
||||
image: vectorim/element-web:latest
|
||||
container_name: fedeo-matrix-element
|
||||
restart: unless-stopped
|
||||
profiles:
|
||||
- matrix
|
||||
volumes:
|
||||
- ./matrix/selfhost/element-config.json:/app/config.json:ro
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.fedeo-matrix-element.rule=Host(`${DOMAIN}`) && PathPrefix(`/element`)
|
||||
- traefik.http.routers.fedeo-matrix-element.entrypoints=websecure
|
||||
- traefik.http.routers.fedeo-matrix-element.tls.certresolver=letsencrypt
|
||||
- traefik.http.middlewares.fedeo-matrix-element-strip.stripprefix.prefixes=/element
|
||||
- traefik.http.routers.fedeo-matrix-element.middlewares=fedeo-matrix-element-strip
|
||||
- traefik.http.services.fedeo-matrix-element.loadbalancer.server.port=80
|
||||
- traefik.docker.network=fedeo_web
|
||||
networks:
|
||||
- web
|
||||
|
||||
networks:
|
||||
web:
|
||||
name: fedeo_web
|
||||
|
||||
@@ -267,7 +267,6 @@ const selectItem = (item) => {
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => selectItem(row.original)"
|
||||
style="height: 70vh"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
>
|
||||
<template #type-cell="{ row }">
|
||||
{{ dataStore.documentTypesForCreation[row.original.type].labelSingle }}
|
||||
@@ -312,6 +311,9 @@ const selectItem = (item) => {
|
||||
<template #amount-cell="{ row }">
|
||||
<span v-if="!deliveryNoteLikeDocumentTypes.includes(row.original.type)">{{ useCurrency(useSum().getCreatedDocumentSum(row.original, createddocuments)) }}</span>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
|
||||
|
||||
@@ -254,7 +254,6 @@ const selectAllocation = (allocationLike) => {
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:on-select="selectAllocation"
|
||||
class="w-full"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen im ausgewählten Zeitraum' }"
|
||||
>
|
||||
<template #amount-cell="{ row }">
|
||||
<span class="text-right text-error" v-if="row.original.amount < 0 || row.original.color === 'red'">{{ useCurrency(row.original.amount) }}</span>
|
||||
@@ -275,6 +274,9 @@ const selectAllocation = (allocationLike) => {
|
||||
<div class="max-w-[22rem] truncate">{{ hasContent(row.original.description) ? row.original.description : '-' }}</div>
|
||||
</UTooltip>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Buchungen im ausgewählten Zeitraum" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,7 +69,6 @@ const columns = [
|
||||
class="mt-3"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:data="props.item.times"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Noch keine Einträge' }"
|
||||
>
|
||||
<template #state-cell="{ row }">
|
||||
<span
|
||||
@@ -102,6 +101,9 @@ const columns = [
|
||||
<template #project-cell="{ row }">
|
||||
{{ row.original.project ? row.original.project.name : "" }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Noch keine Einträge" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="handleSelect"
|
||||
:empty="`Keine ${dataType.label} anzuzeigen`"
|
||||
>
|
||||
<template #name-cell="{ row }">
|
||||
<span
|
||||
@@ -161,6 +160,9 @@
|
||||
</span>
|
||||
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState :label="`Keine ${dataType.label} anzuzeigen`" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
|
||||
19
frontend/components/TableEmptyState.vue
Normal file
19
frontend/components/TableEmptyState.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'i-heroicons-circle-stack-20-solid'
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-2 py-6 text-center text-muted">
|
||||
<UIcon :name="icon" class="size-6" />
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -224,7 +224,6 @@ setupPage()
|
||||
v-if="!loading"
|
||||
:data="reportRows"
|
||||
:columns="columns"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Eingangsbelege mit dieser Kostenstelle oder ihren Unterkostenstellen gefunden' }"
|
||||
class="w-full"
|
||||
>
|
||||
<template #reference-cell="{ row }">
|
||||
@@ -264,6 +263,9 @@ setupPage()
|
||||
<template #amountGross-cell="{ row }">
|
||||
<div class="text-right font-medium tabular-nums">{{ currency(row.original.amountGross) }}</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Eingangsbelege mit dieser Kostenstelle oder ihren Unterkostenstellen gefunden" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto" />
|
||||
|
||||
@@ -25,8 +25,11 @@ setupPage()
|
||||
:data="openTasks"
|
||||
:columns="normalizeTableColumns([{key:'name',label:'Name'},{key:'categorie',label:'Kategorie'}])"
|
||||
:on-select="(i) => router.push(`/tasks/show/${i.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine offenen Aufgaben' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine offenen Aufgaben" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -585,7 +585,6 @@ onMounted(setupPage)
|
||||
:columns="normalizeTableColumns(accountColumns)"
|
||||
:loading="loading"
|
||||
:on-select="openAccount"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungskonten im ausgewählten Zeitraum' }"
|
||||
>
|
||||
<template #label-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||
@@ -606,6 +605,9 @@ onMounted(setupPage)
|
||||
<template #gross-cell="{ row }">
|
||||
<div class="text-right font-medium tabular-nums">{{ useCurrency(row.original.gross) }}</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Buchungskonten im ausgewählten Zeitraum" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</UCard>
|
||||
@@ -624,7 +626,6 @@ onMounted(setupPage)
|
||||
:columns="normalizeTableColumns(ownAccountColumns)"
|
||||
:loading="loading"
|
||||
:on-select="openOwnAccount"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine eigenen Buchungen im ausgewählten Zeitraum' }"
|
||||
>
|
||||
<template #label-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.label }}</div>
|
||||
@@ -647,6 +648,9 @@ onMounted(setupPage)
|
||||
{{ useCurrency(row.original.balance) }}
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine eigenen Buchungen im ausgewählten Zeitraum" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</UCard>
|
||||
@@ -664,11 +668,13 @@ onMounted(setupPage)
|
||||
:data="depreciationRows"
|
||||
:columns="normalizeTableColumns(depreciationColumns)"
|
||||
:loading="loading"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Abschreibungen im ausgewählten Zeitraum' }"
|
||||
>
|
||||
<template #amount-cell="{ row }">
|
||||
<div class="text-right text-amber-600 dark:text-amber-400 tabular-nums">{{ useCurrency(row.original.amount) }}</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Abschreibungen im ausgewählten Zeitraum" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
@@ -117,7 +117,6 @@ onMounted(loadCashbooks)
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => router.push(`/accounting/cashbooks/${row.original?.id || row.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-banknotes', label: 'Keine Kassenbücher angelegt' }"
|
||||
>
|
||||
<template #datevNumber-cell="{ row }">
|
||||
<span class="font-mono">{{ row.original.datevNumber }}</span>
|
||||
@@ -128,6 +127,9 @@ onMounted(loadCashbooks)
|
||||
<template #syncedAt-cell="{ row }">
|
||||
{{ row.original.createdAt ? new Date(row.original.createdAt).toLocaleDateString("de-DE") : "-" }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Kassenbücher angelegt" icon="i-heroicons-banknotes" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="createCashbookModalOpen">
|
||||
|
||||
@@ -255,7 +255,6 @@ onMounted(loadData)
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:data="periods"
|
||||
:loading="loading"
|
||||
:empty="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }"
|
||||
>
|
||||
<template #label-cell="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -281,6 +280,9 @@ onMounted(loadData)
|
||||
<template #documents-cell="{ row }">
|
||||
{{ row.original.outputCount }} / {{ row.original.inputCount }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Daten für die USt-Auswertung vorhanden" icon="i-heroicons-calculator" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
@@ -199,7 +199,6 @@ setupPage()
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => router.push(`/accounts/show/${row.original.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
|
||||
>
|
||||
<template #allocations-cell="{row}">
|
||||
<span v-if="dataLoaded">{{row.original.allocations ? row.original.allocations : null}}</span>
|
||||
@@ -210,6 +209,9 @@ setupPage()
|
||||
<span v-if="dataLoaded">{{row.original.allocations ? useCurrency(row.original.saldo) : null}}</span>
|
||||
<USkeleton v-else class="h-4 w-[250px]" />
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Buchungen anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
|
||||
|
||||
@@ -347,8 +347,11 @@ onMounted(async () => {
|
||||
])"
|
||||
:on-select="(row) => router.push(`/administration/users/${row.original?.id || row.id}`)"
|
||||
class="mt-4"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine zugeordneten Benutzer gefunden' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine zugeordneten Benutzer gefunden" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
<USkeleton v-if="loading" class="h-80" />
|
||||
|
||||
@@ -179,8 +179,11 @@ onMounted(async () => {
|
||||
:columns="normalizeTableColumns(templateColumns)"
|
||||
:loading="loading"
|
||||
:on-select="(row) => router.push(`/administration/tenants/${row.original?.id || row.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-building-office-2', label: 'Keine Tenants gefunden' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Tenants gefunden" icon="i-heroicons-building-office-2" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="createTenantModalOpen">
|
||||
<template #content>
|
||||
|
||||
@@ -136,8 +136,11 @@ onMounted(async () => {
|
||||
:columns="normalizeTableColumns(templateColumns)"
|
||||
:loading="loading"
|
||||
:on-select="(row) => router.push(`/administration/users/${row.original?.id || row.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-users', label: 'Keine Benutzer gefunden' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Benutzer gefunden" icon="i-heroicons-users" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="createUserModalOpen">
|
||||
<template #content>
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
<div style="height: 80vh; overflow-y: scroll">
|
||||
<UTable
|
||||
:columns="normalizeTableColumns(getColumnsForTab(item.key))"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
:data="getRowsForTab(item.key)"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
class="w-full"
|
||||
@@ -134,6 +133,9 @@
|
||||
{{ displayCurrency(useSum().getCreatedDocumentOpenAmount(row.original, items)) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -98,7 +98,6 @@
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => router.push(`/createDocument/edit/${row.original?.id || row.id}`)"
|
||||
empty="Keine Belege anzuzeigen"
|
||||
>
|
||||
<template #actions-cell="{ row }">
|
||||
<div @click.stop>
|
||||
@@ -137,6 +136,9 @@
|
||||
<span v-if="row.original.payment_type === 'transfer'">Überweisung</span>
|
||||
<span v-else-if="row.original.payment_type === 'direct-debit'">SEPA - Einzug</span>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="showExecutionModal" :ui="{ width: 'sm:max-w-4xl' }">
|
||||
@@ -211,7 +213,6 @@
|
||||
:get-row-id="(row) => row.id"
|
||||
:ui="{ th: { base: 'whitespace-nowrap' } }"
|
||||
:on-select="toggleExecutionRow"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
>
|
||||
<template #select-header="{ table }">
|
||||
<div class="flex justify-center" @click.stop>
|
||||
@@ -244,6 +245,9 @@
|
||||
<template #plant-cell="{row}">
|
||||
{{ row.original.plant?.name || "-" }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,7 +97,6 @@ const createExport = async () => {
|
||||
{ key: 'documentDate', label: 'Belegdatum' },
|
||||
{ key: 'outgoingsepamandate', label: 'SEPA-Mandat' },
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine SEPA-Belege anzuzeigen' }"
|
||||
>
|
||||
<template #customer-cell="{ row }">
|
||||
{{ row.original.customer?.name || "-" }}
|
||||
@@ -105,6 +104,9 @@ const createExport = async () => {
|
||||
<template #outgoingsepamandate-cell="{ row }">
|
||||
{{ row.original.outgoingsepamandate?.reference || "-" }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine SEPA-Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -154,7 +154,6 @@ const createExport = async () => {
|
||||
{ key: 'type', label: 'Typ' },
|
||||
{ key: 'download', label: 'Download' },
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Exporte anzuzeigen' }"
|
||||
>
|
||||
<template #created_at-cell="{row}">
|
||||
{{dayjs(row.original.created_at).format("DD.MM.YYYY HH:mm")}}
|
||||
@@ -171,6 +170,9 @@ const createExport = async () => {
|
||||
<template #download-cell="{row}">
|
||||
<UButton @click="downloadFile(row.original)">Download</UButton>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Exporte anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<UModal v-model:open="showCreateExportModal">
|
||||
|
||||
@@ -278,7 +278,6 @@ const selectIncomingInvoice = (invoiceLike) => {
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="selectIncomingInvoice"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
|
||||
>
|
||||
<template #reference-cell="{row}">
|
||||
<span v-if="row.original === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.original.reference}}</span>
|
||||
@@ -310,6 +309,9 @@ const selectIncomingInvoice = (invoiceLike) => {
|
||||
<span v-if="isPaid(row.original)" class="text-primary-500">Bezahlt</span>
|
||||
<span v-else class="text-rose-600">Offen</span>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Belege anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -92,13 +92,14 @@ const filteredRows = computed(() => {
|
||||
class="w-full"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(i) => router.push(`/projecttypes/show/${i.id}`) "
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Projekttypen anzuzeigen' }"
|
||||
>
|
||||
<template #name-cell="{row}">
|
||||
<span class="text-primary-500 font-bold" v-if="row.original === filteredRows[selectedItem]">{{ row.original.name }}</span>
|
||||
<span v-else>{{ row.original.name }}</span>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Projekttypen anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -203,7 +203,6 @@ setupPage()
|
||||
label: 'Saldo'
|
||||
},
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Bankkonten anzuzeigen' }"
|
||||
>
|
||||
<template #expired-cell="{ row }">
|
||||
<span v-if="row.original.expired" class="text-error-600">Ausgelaufen</span>
|
||||
@@ -221,6 +220,9 @@ setupPage()
|
||||
<template #iban-cell="{ row }">
|
||||
{{ row.original.iban.match(/.{1,5}/g).join(" ") }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Bankkonten anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -87,8 +87,10 @@ const columns = computed(() => templateColumns.filter((column) => selectedColumn
|
||||
class="w-full"
|
||||
:on-select="(i) => navigateTo(`/settings/emailaccounts/edit/${i.id}`)"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine E-Mail Konten anzuzeigen' }"
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine E-Mail Konten anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -197,7 +197,6 @@ onMounted(refreshData)
|
||||
{ key: 'path', label: 'Datei' },
|
||||
{ key: 'actions', label: '' }
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-document', label: 'Keine Briefpapiere gefunden' }"
|
||||
>
|
||||
<template #name-cell="{ row }">
|
||||
<span class="font-medium text-gray-900 dark:text-white">
|
||||
@@ -260,6 +259,9 @@ onMounted(refreshData)
|
||||
</ButtonWithConfirm>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Briefpapiere gefunden" icon="i-heroicons-document" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
|
||||
@@ -163,7 +163,6 @@ const getDocLabel = (type) => {
|
||||
:data="texttemplates"
|
||||
:loading="loading"
|
||||
v-model:expand="expand"
|
||||
:empty="{ icon: 'i-heroicons-document-text', label: 'Keine Textvorlagen gefunden' }"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'name', label: 'Bezeichnung' },
|
||||
{ key: 'documentType', label: 'Verwendung' },
|
||||
@@ -233,6 +232,9 @@ const getDocLabel = (type) => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Textvorlagen gefunden" icon="i-heroicons-document-text" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
|
||||
@@ -79,8 +79,11 @@
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
:loading="pending"
|
||||
:on-select="(row) => navigateTo(`/staff/profiles/${row.original?.id || row.id}`)"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Mitarbeiterprofile gefunden' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Mitarbeiterprofile gefunden" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -296,7 +296,6 @@ await setupPage()
|
||||
<UTable
|
||||
v-if="workingTimeInfo"
|
||||
:data="workingTimeInfo.spans"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Anwesenheiten' }"
|
||||
:columns="normalizeTableColumns([
|
||||
{ key: 'status', label: 'Status' },
|
||||
{ key: 'startedAt', label: 'Start' },
|
||||
@@ -329,6 +328,9 @@ await setupPage()
|
||||
<template #type-cell="{ row }">
|
||||
{{ row.original.type.charAt(0).toUpperCase() + row.original.type.slice(1).replace('_', ' ') }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Anwesenheiten" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanel>
|
||||
</div>
|
||||
|
||||
@@ -241,7 +241,6 @@ onMounted(async () => {
|
||||
{ key: 'type', label: 'Typ' },
|
||||
{ key: 'description', label: 'Beschreibung' },
|
||||
])"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Zeiten anzuzeigen' }"
|
||||
>
|
||||
<template #state-cell="{ row }">
|
||||
<UBadge v-if="row.original.state === 'approved'" color="green" variant="subtle">Genehmigt</UBadge>
|
||||
@@ -285,6 +284,9 @@ onMounted(async () => {
|
||||
<span v-else-if="row.original.type === 'sick'">{{ row.original.sick_reason }}</span>
|
||||
<span v-else>{{ row.original.description }}</span>
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Zeiten anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
|
||||
|
||||
@@ -439,14 +439,13 @@ const isDistinctFilterActive = (columnKey) => {
|
||||
<UTable
|
||||
:loading="loading"
|
||||
v-model:sorting="sorting"
|
||||
v-if="dataType && columns && items.length > 0 && !loading"
|
||||
v-if="dataType && columns && !loading"
|
||||
:data="items"
|
||||
:columns="normalizeTableColumns(columns)"
|
||||
class="w-full"
|
||||
style="height: 85dvh"
|
||||
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
|
||||
:on-select="(row) => router.push(`/standardEntity/${type}/show/${row.original.id}`)"
|
||||
:empty="`Keine ${dataType.label} anzuzeigen`"
|
||||
>
|
||||
<template
|
||||
v-for="column in dataType.templateColumns.filter(i => !i.disabledInTable)"
|
||||
@@ -566,23 +565,10 @@ const isDistinctFilterActive = (columnKey) => {
|
||||
</span>
|
||||
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState :label="`Keine ${dataType.label} anzuzeigen`" />
|
||||
</template>
|
||||
</UTable>
|
||||
<UCard
|
||||
class="w-1/3 mx-auto mt-10"
|
||||
v-else-if="!loading"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col text-center"
|
||||
>
|
||||
<UIcon
|
||||
class="mx-auto w-10 h-10 mb-5"
|
||||
name="i-heroicons-circle-stack-20-solid"/>
|
||||
<span class="font-bold">Keine {{dataType.label}} anzuzeigen</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</UCard>
|
||||
<UProgress v-else animation="carousel" class="w-3/4 mx-auto mt-5"></UProgress>
|
||||
</div>
|
||||
<div v-else class="relative flex flex-col h-[calc(100dvh-80px)]">
|
||||
@@ -679,8 +665,7 @@ const isDistinctFilterActive = (columnKey) => {
|
||||
v-if="!loading && items.length === 0"
|
||||
class="mx-auto mt-10 p-6 text-center"
|
||||
>
|
||||
<UIcon name="i-heroicons-circle-stack-20-solid" class="mx-auto w-10 h-10 mb-3"/>
|
||||
<p class="font-bold">Keine {{ dataType.label }} gefunden</p>
|
||||
<TableEmptyState :label="`Keine ${dataType.label} anzuzeigen`" />
|
||||
</UCard>
|
||||
|
||||
<div v-if="loading" class="mt-5">
|
||||
|
||||
@@ -73,7 +73,6 @@ const filteredRows = computed(() => {
|
||||
</UDashboardToolbar>
|
||||
<UTable
|
||||
:data="filteredRows"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: `Keine Tickets anzuzeigen` }"
|
||||
:on-select="(i) => router.push(`/support/${i.id}`)"
|
||||
:columns="normalizeTableColumns([{key:'created_at',label:'Datum'}, ...profileStore.currentTenant === 5 ? [{key:'tenant',label:'Tenant'}] : [],{key:'status',label:'Status'},{key:'title',label:'Titel'},{key:'created_by',label:'Ersteller'},{key:'ticketmessages',label:'Nachrichten'}])"
|
||||
>
|
||||
@@ -93,6 +92,9 @@ const filteredRows = computed(() => {
|
||||
<template #ticketmessages-cell="{ row }">
|
||||
{{row.original.ticketmessages.length}}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState :label="`Keine Tickets anzuzeigen`" />
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -461,7 +461,6 @@ onMounted(async () => {
|
||||
:data="filteredTasks"
|
||||
:columns="normalizedListColumns"
|
||||
:on-select="(task) => openTaskViaRoute(task)"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Aufgaben anzuzeigen' }"
|
||||
>
|
||||
<template #actions-cell="{ row }">
|
||||
<UButton
|
||||
@@ -490,13 +489,19 @@ onMounted(async () => {
|
||||
<template #plant-cell="{ row }">
|
||||
{{ getEntityLabel(plantOptions, row.original.plant?.id || row.original.plant) || "-" }}
|
||||
</template>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Aufgaben anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
<UTable
|
||||
v-else
|
||||
:data="[]"
|
||||
:columns="normalizedListColumns"
|
||||
:empty="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Aufgaben anzuzeigen' }"
|
||||
/>
|
||||
>
|
||||
<template #empty>
|
||||
<TableEmptyState label="Keine Aufgaben anzuzeigen" />
|
||||
</template>
|
||||
</UTable>
|
||||
</UDashboardPanelContent>
|
||||
|
||||
<UModal v-model:open="isModalOpen" :prevent-close="saving || deleting">
|
||||
|
||||
21
matrix/selfhost/element-config.json
Normal file
21
matrix/selfhost/element-config.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"default_server_config": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https://app.example.com",
|
||||
"server_name": "app.example.com"
|
||||
}
|
||||
},
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://app.example.com/livekit/jwt"
|
||||
}
|
||||
],
|
||||
"disable_custom_urls": false,
|
||||
"disable_guests": true,
|
||||
"brand": "FEDEO Matrix",
|
||||
"default_theme": "light",
|
||||
"features": {
|
||||
"feature_video_rooms": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user