mirror of
https://github.com/open-reception/appointment-booking-software.git
synced 2026-09-23 17:34:51 +02:00
Make descriptions multilingual (#89)
* Make descriptions multilingual * Format and test fixes * Update src/lib/server/db/tenant-schema.ts * Fix tenant-switcher to update instantly * Fix sql migrations for description * Add languages to tenant table * Fix tenant schema * Use objects for translated fields * Fix format * Fix lint * Allow longer descriptions for channels * Field Localizations are based on tenant locales not supportedLocales * Fix agent image list preview * Improve language tab keyboard navigation * Fix falsy redirect after store init --------- Co-authored-by: Karl Ludwig Weise <office@ludwigweise.de> Co-authored-by: Karl Ludwig Weise <ludwig@ludwigweise.de>
This commit is contained in:
co-authored by
Karl Ludwig Weise
Karl Ludwig Weise
parent
d2cc794aec
commit
5965fada38
@@ -0,0 +1,9 @@
|
||||
# Database
|
||||
|
||||
## Generating migrations
|
||||
|
||||
1. Make your changes to the respective drizzle schema
|
||||
2. Fix issues in code
|
||||
3. To create migrations
|
||||
- for the central database run `npm run db:generate`
|
||||
- for tenant databases run `npm run db:tenant:generate`
|
||||
@@ -0,0 +1,10 @@
|
||||
ALTER TABLE "tenant"
|
||||
ALTER COLUMN "description"
|
||||
SET DATA TYPE json
|
||||
USING CASE
|
||||
WHEN description IS NULL THEN NULL
|
||||
WHEN description::text = '' THEN NULL
|
||||
ELSE description::json
|
||||
END;--> statement-breakpoint
|
||||
ALTER TABLE "tenant" RENAME COLUMN "description" TO "descriptions";--> statement-breakpoint
|
||||
ALTER TABLE "tenant" ADD COLUMN "languages" json NOT NULL;
|
||||
@@ -0,0 +1,773 @@
|
||||
{
|
||||
"id": "805d1d9a-094d-4a4d-a998-2f74ca7dde8d",
|
||||
"prevId": "a1a8c8d8-96ed-4dfa-9fa4-047ff0a02d7e",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.tenant": {
|
||||
"name": "tenant",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"short_name": {
|
||||
"name": "short_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"long_name": {
|
||||
"name": "long_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"descriptions": {
|
||||
"name": "descriptions",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"languages": {
|
||||
"name": "languages",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"logo": {
|
||||
"name": "logo",
|
||||
"type": "varchar(100000)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"database_url": {
|
||||
"name": "database_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"setup_state": {
|
||||
"name": "setup_state",
|
||||
"type": "setup_state",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'NEW'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tenant_database_url_idx": {
|
||||
"name": "tenant_database_url_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "database_url",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"tenant_short_name_unique": {
|
||||
"name": "tenant_short_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["short_name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tenant_config": {
|
||||
"name": "tenant_config",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"tenant_id": {
|
||||
"name": "tenant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "config_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tenant_config_tenant_name_idx": {
|
||||
"name": "tenant_config_tenant_name_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "tenant_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "name",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tenant_config_tenant_id_tenant_id_fk": {
|
||||
"name": "tenant_config_tenant_id_tenant_id_fk",
|
||||
"tableFrom": "tenant_config",
|
||||
"tableTo": "tenant",
|
||||
"columnsFrom": ["tenant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "user_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'STAFF'"
|
||||
},
|
||||
"tenant_id": {
|
||||
"name": "tenant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_login_at": {
|
||||
"name": "last_login_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": true
|
||||
},
|
||||
"confirmed": {
|
||||
"name": "confirmed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"token_valid_until": {
|
||||
"name": "token_valid_until",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"passphrase_hash": {
|
||||
"name": "passphrase_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"recovery_passphrase": {
|
||||
"name": "recovery_passphrase",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'de'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email_idx": {
|
||||
"name": "user_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_tenant_id_tenant_id_fk": {
|
||||
"name": "user_tenant_id_tenant_id_fk",
|
||||
"tableFrom": "user",
|
||||
"tableTo": "tenant",
|
||||
"columnsFrom": ["tenant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_invite": {
|
||||
"name": "user_invite",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"invite_code": {
|
||||
"name": "invite_code",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "user_role",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tenant_id": {
|
||||
"name": "tenant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"invited_by": {
|
||||
"name": "invited_by",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'de'"
|
||||
},
|
||||
"used": {
|
||||
"name": "used",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"used_at": {
|
||||
"name": "used_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_user_id": {
|
||||
"name": "created_user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_invite_code_idx": {
|
||||
"name": "user_invite_code_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "invite_code",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"user_invite_email_idx": {
|
||||
"name": "user_invite_email_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"user_invite_tenant_idx": {
|
||||
"name": "user_invite_tenant_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "tenant_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_invite_tenant_id_tenant_id_fk": {
|
||||
"name": "user_invite_tenant_id_tenant_id_fk",
|
||||
"tableFrom": "user_invite",
|
||||
"tableTo": "tenant",
|
||||
"columnsFrom": ["tenant_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_invite_invited_by_user_id_fk": {
|
||||
"name": "user_invite_invited_by_user_id_fk",
|
||||
"tableFrom": "user_invite",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["invited_by"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_invite_created_user_id_user_id_fk": {
|
||||
"name": "user_invite_created_user_id_user_id_fk",
|
||||
"tableFrom": "user_invite",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["created_user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_invite_invite_code_unique": {
|
||||
"name": "user_invite_invite_code_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["invite_code"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_passkey": {
|
||||
"name": "user_passkey",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"counter": {
|
||||
"name": "counter",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"device_name": {
|
||||
"name": "device_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_passkey_user_idx": {
|
||||
"name": "user_passkey_user_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_passkey_user_id_user_id_fk": {
|
||||
"name": "user_passkey_user_id_user_id_fk",
|
||||
"tableFrom": "user_passkey",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_session": {
|
||||
"name": "user_session",
|
||||
"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": true
|
||||
},
|
||||
"session_token": {
|
||||
"name": "session_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_session_user_idx": {
|
||||
"name": "user_session_user_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "user_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"user_session_token_idx": {
|
||||
"name": "user_session_token_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "session_token",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_session_user_id_user_id_fk": {
|
||||
"name": "user_session_user_id_user_id_fk",
|
||||
"tableFrom": "user_session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_session_session_token_unique": {
|
||||
"name": "user_session_session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["session_token"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.config_type": {
|
||||
"name": "config_type",
|
||||
"schema": "public",
|
||||
"values": ["BOOLEAN", "NUMBER", "STRING"]
|
||||
},
|
||||
"public.setup_state": {
|
||||
"name": "setup_state",
|
||||
"schema": "public",
|
||||
"values": ["NEW", "SETTINGS_CREATED", "AGENTS_SET_UP", "FIRST_CHANNEL_CREATED"]
|
||||
},
|
||||
"public.user_role": {
|
||||
"name": "user_role",
|
||||
"schema": "public",
|
||||
"values": ["GLOBAL_ADMIN", "TENANT_ADMIN", "STAFF"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,13 @@
|
||||
"when": 1758555636975,
|
||||
"tag": "0003_strange_lord_hawal",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1759144205727,
|
||||
"tag": "0004_mixed_caretaker",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Generated
+29
-12
@@ -45,7 +45,7 @@
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^24",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"bits-ui": "^2.8.13",
|
||||
"bits-ui": "^2.11.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.29.0",
|
||||
@@ -3487,17 +3487,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bits-ui": {
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.8.13.tgz",
|
||||
"integrity": "sha512-YC3DiatZzREdbTZR8IuxbQeYg+/MNcyQeuS6Hycl+ufoG7eiV66qICx210plFER9B776FEYSHXSYuQdlvWODpw==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.11.4.tgz",
|
||||
"integrity": "sha512-OlVBJhNUMDHbIAf8oDAyPchIrU8b1S5NAMm6enMZSKx5HKcf/QPI485/BL1r4EPlv4O3m45e59hBRCETtYFdxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.1",
|
||||
"@floating-ui/dom": "^1.7.1",
|
||||
"esm-env": "^1.1.2",
|
||||
"runed": "^0.29.1",
|
||||
"svelte-toolbelt": "^0.9.3",
|
||||
"runed": "^0.31.1",
|
||||
"svelte-toolbelt": "^0.10.4",
|
||||
"tabbable": "^6.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3512,9 +3512,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bits-ui/node_modules/runed": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.29.2.tgz",
|
||||
"integrity": "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==",
|
||||
"version": "0.31.1",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.31.1.tgz",
|
||||
"integrity": "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
@@ -3529,9 +3529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bits-ui/node_modules/svelte-toolbelt": {
|
||||
"version": "0.9.3",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.9.3.tgz",
|
||||
"integrity": "sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.5.tgz",
|
||||
"integrity": "sha512-8e+eWTgxw1aiLxhDE8Rb1X6AoLitqpJz+WhAul2W7W58C8KoLoJQf1TgQdFPBiCPJ0Jg5y0Zi1uyua9em4VS0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte"
|
||||
@@ -3549,6 +3549,23 @@
|
||||
"svelte": "^5.30.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bits-ui/node_modules/svelte-toolbelt/node_modules/runed": {
|
||||
"version": "0.29.2",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.29.2.tgz",
|
||||
"integrity": "sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
"https://github.com/sponsors/tglide"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esm-env": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
|
||||
+4
-2
@@ -21,6 +21,7 @@
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:tenant:generate": "drizzle-kit generate --config=drizzle.tenant.config.ts",
|
||||
"db:drop": "docker volume ls -q | grep '^open-reception_' | xargs -r docker volume rm",
|
||||
"docker:dev:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||
"docker:dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"docker:dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
|
||||
@@ -33,7 +34,8 @@
|
||||
"docker:prod:down": "docker compose -f docker-compose.prod.yml down",
|
||||
"docker:prod:logs": "docker compose -f docker-compose.prod.yml logs -f",
|
||||
"docker:prod:clean": "docker compose -f docker-compose.prod.yml down -v --remove-orphans",
|
||||
"i18n:compile": "npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/i18n"
|
||||
"i18n:compile": "npx @inlang/paraglide-js compile --project ./project.inlang --outdir ./src/i18n",
|
||||
"db:clean": "npm run docker:dev:down && npm run db:drop && npm run docker:dev:up"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.0",
|
||||
@@ -52,7 +54,7 @@
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/node": "^24",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"bits-ui": "^2.8.13",
|
||||
"bits-ui": "^2.11.4",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"eslint": "^9.29.0",
|
||||
|
||||
@@ -275,5 +275,9 @@
|
||||
"error": "Konnte Akteur:in nicht aktualisieren.",
|
||||
"action": "Übernehmen"
|
||||
}
|
||||
},
|
||||
"locales": {
|
||||
"en": "Englisch",
|
||||
"de": "Deutsch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,5 +284,9 @@
|
||||
"error": "Could not update agent.",
|
||||
"action": "Edit Agent"
|
||||
}
|
||||
},
|
||||
"locales": {
|
||||
"en": "English",
|
||||
"de": "German"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,8 @@
|
||||
const sidebar = useSidebar();
|
||||
|
||||
const maxTenantsToShow = 5;
|
||||
let activeTenantId = $auth.user?.tenantId;
|
||||
let activeTenantId = $derived($auth.user?.tenantId);
|
||||
let activeTenant = $derived($tenants.tenants.find((t) => t.id === activeTenantId));
|
||||
|
||||
$effect(() => {
|
||||
activeTenantId = $auth.user?.tenantId;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Sidebar.Menu>
|
||||
@@ -86,9 +82,9 @@
|
||||
side={sidebar.isMobile ? "bottom" : "right"}
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenu.Label class="text-muted-foreground text-xs"
|
||||
>{m["nav.tenants"]()}</DropdownMenu.Label
|
||||
>
|
||||
<DropdownMenu.Label class="text-muted-foreground text-xs">
|
||||
{m["nav.tenants"]()}
|
||||
</DropdownMenu.Label>
|
||||
{#each $tenants?.tenants.slice(0, maxTenantsToShow) as tenant (tenant.id)}
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => tenants.setCurrentTenant(tenant.id)}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import LanguageTabs from "./language-tabs.svelte";
|
||||
|
||||
export { LanguageTabs };
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
import { translatedLocales } from "$lib/const/locales";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import type { Snippet } from "svelte";
|
||||
import { Separator } from "../separator";
|
||||
|
||||
const tenantLanguages = ($tenants.currentTenant?.languages || []).map((it) => ({
|
||||
key: it,
|
||||
label: translatedLocales[it as keyof typeof translatedLocales],
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
children: Snippet<[{ locale: string }]>;
|
||||
};
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if tenantLanguages.length > 1}
|
||||
<Tabs.Root value={tenantLanguages[0]?.key}>
|
||||
<Tabs.List>
|
||||
{#each tenantLanguages as language (language.key)}
|
||||
<Tabs.Trigger value={language.key}>{language.label}</Tabs.Trigger>
|
||||
{/each}
|
||||
</Tabs.List>
|
||||
{#each tenantLanguages as language (language.key)}
|
||||
<Tabs.Content value={language.key}>
|
||||
{@render children({ locale: language.key })}
|
||||
</Tabs.Content>
|
||||
{/each}
|
||||
</Tabs.Root>
|
||||
{:else if tenantLanguages.length === 1}
|
||||
{@render children({ locale: tenantLanguages[0].key })}
|
||||
{/if}
|
||||
<Separator class="mb-3" />
|
||||
@@ -0,0 +1,16 @@
|
||||
import Root from "./tabs.svelte";
|
||||
import Content from "./tabs-content.svelte";
|
||||
import List from "./tabs-list.svelte";
|
||||
import Trigger from "./tabs-trigger.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as Tabs,
|
||||
Content as TabsContent,
|
||||
List as TabsList,
|
||||
Trigger as TabsTrigger,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.ContentProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn("flex-1 outline-none", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let { ref = $bindable(null), class: className, ...restProps }: TabsPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
class={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="tabs-trigger"
|
||||
class={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
data-slot="tabs"
|
||||
class={cn("flex flex-col gap-2", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { m } from "$i18n/messages";
|
||||
|
||||
export const supportedLocales = ["en", "de"] as const;
|
||||
|
||||
export const translatedLocales = {
|
||||
en: m["locales.en"](),
|
||||
de: m["locales.de"](),
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
integer,
|
||||
index,
|
||||
varchar,
|
||||
json,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
/**
|
||||
@@ -44,8 +45,10 @@ export const tenant = pgTable(
|
||||
shortName: text("short_name").notNull().unique(),
|
||||
/** Full organization name displayed to users */
|
||||
longName: text("long_name").notNull(),
|
||||
/** Optional description of the organization */
|
||||
description: text("description"),
|
||||
/** Optional descriptions of the organization */
|
||||
descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(),
|
||||
/** Active languages for this tenant (array of language codes) */
|
||||
languages: json("languages").$type<string[]>().notNull(),
|
||||
/** Organization logo as binary data (PNG, JPEG, GIF, or WEBP) */
|
||||
logo: varchar("logo", { length: 100_000 }),
|
||||
/** Database connection string for this tenant's isolated database */
|
||||
|
||||
@@ -38,9 +38,9 @@ export const agent = pgTable("agent", {
|
||||
/** Agent's display name */
|
||||
name: text("name").notNull(),
|
||||
/** Optional description of the agent's role or specialties */
|
||||
description: text("description"),
|
||||
descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(),
|
||||
/** Optional logo/profile image for the agent (PNG, JPEG, GIF, or WEBP) */
|
||||
image: varchar("image", { length: 100_000 }),
|
||||
image: varchar("image", { length: 250_000 }),
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -54,15 +54,13 @@ export const channel = pgTable("channel", {
|
||||
/** Primary key - unique identifier */
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
/** Channel display names in multiple languages (array of strings in same order as languages) */
|
||||
names: json("names").$type<string[]>().notNull(),
|
||||
names: json("names").$type<{ [key: string]: string }>().notNull(),
|
||||
/** Optional color for UI display (hex code) */
|
||||
color: text("color"),
|
||||
/** Whether the channel is paused and does not offer nor accept new appointments */
|
||||
pause: boolean("paused").notNull().default(false),
|
||||
/** Optional descriptions in multiple languages (array of strings in same order as languages) */
|
||||
descriptions: json("descriptions").$type<string[]>(),
|
||||
/** Active languages for this channel (array of language codes) */
|
||||
languages: json("languages").$type<string[]>().notNull(),
|
||||
descriptions: json("descriptions").$type<{ [key: string]: string }>().notNull(),
|
||||
/** Whether channel is publicly bookable or requires internal access */
|
||||
isPublic: boolean("is_public"),
|
||||
/** Whether appointments must be explicitly confirmed by staff */
|
||||
|
||||
@@ -107,7 +107,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -137,7 +138,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -167,7 +169,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -197,7 +200,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -252,7 +256,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -315,7 +320,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -354,7 +360,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -387,7 +394,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -420,7 +428,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -472,7 +481,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
@@ -515,7 +525,8 @@ describe("Email System", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Organization",
|
||||
description: null,
|
||||
descriptions: { en: "Tenant Description" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "FIRST_CHANNEL_CREATED" as const,
|
||||
|
||||
@@ -29,7 +29,8 @@ describe("generateBaseUrl", () => {
|
||||
logo: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
};
|
||||
@@ -51,7 +52,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "test",
|
||||
longName: "Test Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -69,7 +71,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -87,7 +90,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -125,7 +129,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -143,7 +148,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -161,7 +167,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-2",
|
||||
shortName: "new-tenant",
|
||||
longName: "New Tenant Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -179,7 +186,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-2",
|
||||
shortName: "new-tenant",
|
||||
longName: "New Tenant Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -197,7 +205,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "tenant",
|
||||
longName: "Tenant Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -215,7 +224,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -233,7 +243,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "", // Empty shortName
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -254,7 +265,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -273,7 +285,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
@@ -292,7 +305,8 @@ describe("generateBaseUrl", () => {
|
||||
id: "tenant-1",
|
||||
shortName: "acme",
|
||||
longName: "ACME Corp",
|
||||
description: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "",
|
||||
setupState: "NEW",
|
||||
logo: null,
|
||||
|
||||
@@ -6,7 +6,8 @@ describe("sendTenantAdminInviteEmail", () => {
|
||||
id: "test-tenant-id",
|
||||
shortName: "testcorp",
|
||||
longName: "Test Corporation GmbH",
|
||||
description: "A test corporation",
|
||||
descriptions: { en: "A test corporation" },
|
||||
languages: ["en"],
|
||||
databaseUrl: "postgresql://test",
|
||||
setupState: "NEW" as const,
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -58,8 +58,9 @@ const mockDb = {
|
||||
const mockAgent = {
|
||||
id: "agent-123",
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
logo: Buffer.from("test"),
|
||||
descriptions: ["Test description"],
|
||||
languages: ["en"],
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
|
||||
describe("AgentService", () => {
|
||||
@@ -100,7 +101,8 @@ describe("AgentService", () => {
|
||||
|
||||
const request = {
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
descriptions: { en: "Test description" },
|
||||
languages: ["en"],
|
||||
image: "test",
|
||||
};
|
||||
|
||||
@@ -110,7 +112,7 @@ describe("AgentService", () => {
|
||||
expect(mockDb.insert).toHaveBeenCalled();
|
||||
expect(insertChain.values).toHaveBeenCalledWith({
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
descriptions: { en: "Test description" },
|
||||
image: "test",
|
||||
});
|
||||
});
|
||||
@@ -118,7 +120,7 @@ describe("AgentService", () => {
|
||||
it("should handle validation error for invalid name", async () => {
|
||||
const request = {
|
||||
name: "",
|
||||
description: "Test description",
|
||||
description: ["Test description"],
|
||||
};
|
||||
|
||||
await expect(service.createAgent(request)).rejects.toThrow(ValidationError);
|
||||
|
||||
@@ -45,10 +45,9 @@ import { getTenantDb } from "../../db";
|
||||
// Mock data with valid UUIDs
|
||||
const mockChannel = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
names: ["Test Channel"],
|
||||
names: { en: "Test Channel" },
|
||||
color: "#FF0000",
|
||||
descriptions: ["Test description"],
|
||||
languages: ["de"],
|
||||
descriptions: { en: "Test description" },
|
||||
isPublic: true,
|
||||
requiresConfirmation: false,
|
||||
pause: false,
|
||||
@@ -57,7 +56,7 @@ const mockChannel = {
|
||||
const mockAgent = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440001",
|
||||
name: "Test Agent",
|
||||
description: "Test description",
|
||||
descriptions: { en: "Test description" },
|
||||
image: null,
|
||||
};
|
||||
|
||||
@@ -118,8 +117,7 @@ describe("ChannelService", () => {
|
||||
|
||||
// Use minimal valid request that matches schema exactly
|
||||
const request = {
|
||||
names: ["Test Channel"],
|
||||
languages: ["de"],
|
||||
names: { en: "Test Channel" },
|
||||
};
|
||||
|
||||
const result = await service.createChannel(request as any);
|
||||
@@ -138,8 +136,7 @@ describe("ChannelService", () => {
|
||||
mockDb.transaction.mockResolvedValue(expectedResult);
|
||||
|
||||
const request = {
|
||||
names: ["Test Channel"],
|
||||
languages: ["de"],
|
||||
names: { en: "Test Channel" },
|
||||
agentIds: ["550e8400-e29b-41d4-a716-446655440001"],
|
||||
slotTemplates: [
|
||||
{
|
||||
@@ -159,8 +156,7 @@ describe("ChannelService", () => {
|
||||
|
||||
it("should handle validation error for invalid name", async () => {
|
||||
const request = {
|
||||
names: [""],
|
||||
languages: ["de"],
|
||||
names: { en: "" },
|
||||
agentIds: [],
|
||||
slotTemplates: [],
|
||||
};
|
||||
@@ -170,8 +166,7 @@ describe("ChannelService", () => {
|
||||
|
||||
it("should handle validation error for invalid time format", async () => {
|
||||
const request = {
|
||||
names: ["Test Channel"],
|
||||
languages: ["de"],
|
||||
names: { en: "Test Channel" },
|
||||
agentIds: [],
|
||||
slotTemplates: [
|
||||
{
|
||||
@@ -190,8 +185,7 @@ describe("ChannelService", () => {
|
||||
mockDb.transaction.mockRejectedValue(new Error("Transaction failed"));
|
||||
|
||||
const request = {
|
||||
names: ["Test Channel"],
|
||||
languages: ["de"],
|
||||
names: { en: "Test Channel" },
|
||||
agentIds: [],
|
||||
slotTemplates: [],
|
||||
};
|
||||
@@ -210,7 +204,7 @@ describe("ChannelService", () => {
|
||||
it("should update channel successfully", async () => {
|
||||
const expectedResult = {
|
||||
...mockChannel,
|
||||
names: ["Updated Channel"],
|
||||
names: { en: "Updated Channel" },
|
||||
agents: [],
|
||||
slotTemplates: [],
|
||||
};
|
||||
@@ -218,7 +212,7 @@ describe("ChannelService", () => {
|
||||
mockDb.transaction.mockResolvedValue(expectedResult);
|
||||
|
||||
const updateData = {
|
||||
names: ["Updated Channel"],
|
||||
names: { en: "Updated Channel" },
|
||||
};
|
||||
|
||||
const result = await service.updateChannel(
|
||||
@@ -231,7 +225,7 @@ describe("ChannelService", () => {
|
||||
});
|
||||
|
||||
it("should handle validation error for invalid name", async () => {
|
||||
const updateData = { names: [""], languages: ["de"] };
|
||||
const updateData = { names: { en: "" } };
|
||||
|
||||
await expect(
|
||||
service.updateChannel("550e8400-e29b-41d4-a716-446655440000", updateData),
|
||||
@@ -241,7 +235,7 @@ describe("ChannelService", () => {
|
||||
it("should handle transaction error", async () => {
|
||||
mockDb.transaction.mockRejectedValue(new Error("Transaction failed"));
|
||||
|
||||
const updateData = { names: ["Updated Channel"], languages: ["de"] };
|
||||
const updateData = { names: { en: "Updated Channel" } };
|
||||
|
||||
await expect(
|
||||
service.updateChannel("550e8400-e29b-41d4-a716-446655440000", updateData),
|
||||
|
||||
@@ -67,6 +67,8 @@ describe("TenantAdminService", () => {
|
||||
const newTenant = {
|
||||
shortName: "test-clinic",
|
||||
longName: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
description: "A test clinic",
|
||||
};
|
||||
|
||||
@@ -118,8 +120,7 @@ describe("TenantAdminService", () => {
|
||||
it("should rollback tenant creation if database initialization fails", async () => {
|
||||
const newTenant = {
|
||||
shortName: "test-clinic",
|
||||
longName: "",
|
||||
description: "A test clinic",
|
||||
descriptions: "A test clinic",
|
||||
};
|
||||
|
||||
const mockCreatedTenant = {
|
||||
@@ -237,7 +238,7 @@ describe("TenantAdminService", () => {
|
||||
const tenantId = "tenant-123";
|
||||
const updateData = {
|
||||
longName: "Updated Clinic Name",
|
||||
description: "Updated description",
|
||||
description: ["Updated description"],
|
||||
logo: "logo data",
|
||||
};
|
||||
|
||||
|
||||
@@ -6,17 +6,20 @@ import { eq, and, between, or, lte, gte, ne } from "drizzle-orm";
|
||||
import logger from "$lib/logger";
|
||||
import z from "zod/v4";
|
||||
import { ValidationError, NotFoundError, ConflictError } from "../utils/errors";
|
||||
import { supportedLocales } from "$lib/const/locales";
|
||||
|
||||
const agentCreationSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().optional(),
|
||||
descriptions: z.partialRecord(z.enum(supportedLocales), z.string().min(1)).optional(),
|
||||
image: z.string().optional().nullable(),
|
||||
languages: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const agentUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().optional(),
|
||||
descriptions: z.partialRecord(z.enum(supportedLocales), z.string().min(1)).optional(),
|
||||
image: z.string().optional().nullable(),
|
||||
languages: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
const absenceCreationSchema = z.object({
|
||||
@@ -96,7 +99,7 @@ export class AgentService {
|
||||
.insert(tenantSchema.agent)
|
||||
.values({
|
||||
name: request.name,
|
||||
description: request.description,
|
||||
descriptions: request.descriptions ?? {},
|
||||
image: request.image,
|
||||
})
|
||||
.returning();
|
||||
@@ -295,7 +298,7 @@ export class AgentService {
|
||||
.select({
|
||||
id: tenantSchema.agent.id,
|
||||
name: tenantSchema.agent.name,
|
||||
description: tenantSchema.agent.description,
|
||||
descriptions: tenantSchema.agent.descriptions,
|
||||
image: tenantSchema.agent.image,
|
||||
})
|
||||
.from(tenantSchema.agent)
|
||||
|
||||
@@ -7,6 +7,7 @@ import logger from "$lib/logger";
|
||||
import z from "zod/v4";
|
||||
import { ValidationError, NotFoundError } from "../utils/errors";
|
||||
import { TenantConfig } from "../db/tenant-config";
|
||||
import { supportedLocales } from "$lib/const/locales";
|
||||
|
||||
const CHANNEL_COLORS = ["#FF0000", "#00FF00", "#0000FF"] as const;
|
||||
const NEXT_COLOR_KEY = "nextChannelColor";
|
||||
@@ -19,56 +20,31 @@ const slotTemplateSchema = z.object({
|
||||
duration: z.number().int().min(1).max(1440),
|
||||
});
|
||||
|
||||
const channelCreationSchema = z
|
||||
.object({
|
||||
names: z.array(z.string().min(1).max(100)).min(1),
|
||||
color: z.string().optional(),
|
||||
descriptions: z.array(z.string()).optional(),
|
||||
languages: z.array(z.string().min(2).max(5)).min(1),
|
||||
isPublic: z.boolean().optional(),
|
||||
requiresConfirmation: z.boolean().optional(),
|
||||
agentIds: z.array(z.string().uuid()).optional().default([]),
|
||||
slotTemplates: z.array(slotTemplateSchema).optional().default([]),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
!data.descriptions ||
|
||||
data.descriptions.length === 0 ||
|
||||
data.descriptions.length === data.languages.length,
|
||||
{ message: "descriptions array must have same length as languages array" },
|
||||
)
|
||||
.refine((data) => data.names.length === data.languages.length, {
|
||||
message: "names array must have same length as languages array",
|
||||
});
|
||||
const channelCreationSchema = z.object({
|
||||
names: z.partialRecord(z.enum(supportedLocales), z.string().min(1).max(100)),
|
||||
color: z.string().optional(),
|
||||
descriptions: z.partialRecord(z.enum(supportedLocales), z.string().min(1)).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
requiresConfirmation: z.boolean().optional(),
|
||||
agentIds: z.array(z.string().uuid()).optional().default([]),
|
||||
slotTemplates: z.array(slotTemplateSchema).optional().default([]),
|
||||
});
|
||||
|
||||
const channelUpdateSchema = z
|
||||
.object({
|
||||
names: z.array(z.string().min(1).max(100)).optional(),
|
||||
color: z.string().optional(),
|
||||
descriptions: z.array(z.string()).optional(),
|
||||
languages: z.array(z.string().min(2).max(5)).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
requiresConfirmation: z.boolean().optional(),
|
||||
agentIds: z.array(z.string().uuid()).optional(),
|
||||
slotTemplates: z
|
||||
.array(
|
||||
slotTemplateSchema.extend({
|
||||
id: z.string().uuid().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) =>
|
||||
!data.descriptions ||
|
||||
!data.languages ||
|
||||
data.descriptions.length === 0 ||
|
||||
data.descriptions.length === data.languages.length,
|
||||
{ message: "descriptions array must have same length as languages array" },
|
||||
)
|
||||
.refine((data) => !data.names || !data.languages || data.names.length === data.languages.length, {
|
||||
message: "names array must have same length as languages array",
|
||||
});
|
||||
const channelUpdateSchema = z.object({
|
||||
names: z.partialRecord(z.enum(supportedLocales), z.string().min(1).max(100)),
|
||||
color: z.string().optional(),
|
||||
descriptions: z.partialRecord(z.enum(supportedLocales), z.string().min(1)).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
requiresConfirmation: z.boolean().optional(),
|
||||
agentIds: z.array(z.string().uuid()).optional(),
|
||||
slotTemplates: z
|
||||
.array(
|
||||
slotTemplateSchema.extend({
|
||||
id: z.string().uuid().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ChannelCreationRequest = z.infer<typeof channelCreationSchema>;
|
||||
export type ChannelUpdateRequest = z.infer<typeof channelUpdateSchema>;
|
||||
@@ -133,7 +109,6 @@ export class ChannelService {
|
||||
log.debug("Creating new channel", {
|
||||
tenantId: this.tenantId,
|
||||
names: request.names,
|
||||
languages: request.languages,
|
||||
agentCount: request.agentIds?.length || 0,
|
||||
slotTemplateCount: request.slotTemplates?.length || 0,
|
||||
});
|
||||
@@ -149,8 +124,7 @@ export class ChannelService {
|
||||
.values({
|
||||
names: request.names,
|
||||
color: request.color,
|
||||
descriptions: request.descriptions || [],
|
||||
languages: request.languages,
|
||||
descriptions: request.descriptions || {},
|
||||
isPublic: request.isPublic,
|
||||
requiresConfirmation: request.requiresConfirmation,
|
||||
})
|
||||
@@ -218,7 +192,6 @@ export class ChannelService {
|
||||
tenantId: this.tenantId,
|
||||
channelId: result.id,
|
||||
names: result.names,
|
||||
languages: result.languages,
|
||||
agentCount: result.agents.length,
|
||||
slotTemplateCount: result.slotTemplates.length,
|
||||
});
|
||||
@@ -266,7 +239,6 @@ export class ChannelService {
|
||||
names: updateData.names,
|
||||
color: updateData.color,
|
||||
descriptions: updateData.descriptions,
|
||||
languages: updateData.languages,
|
||||
isPublic: updateData.isPublic,
|
||||
requiresConfirmation: updateData.requiresConfirmation,
|
||||
};
|
||||
@@ -338,7 +310,7 @@ export class ChannelService {
|
||||
.select({
|
||||
id: tenantSchema.agent.id,
|
||||
name: tenantSchema.agent.name,
|
||||
description: tenantSchema.agent.description,
|
||||
descriptions: tenantSchema.agent.descriptions,
|
||||
image: tenantSchema.agent.image,
|
||||
})
|
||||
.from(tenantSchema.agent)
|
||||
@@ -515,7 +487,7 @@ export class ChannelService {
|
||||
.select({
|
||||
id: tenantSchema.agent.id,
|
||||
name: tenantSchema.agent.name,
|
||||
description: tenantSchema.agent.description,
|
||||
descriptions: tenantSchema.agent.descriptions,
|
||||
image: tenantSchema.agent.image,
|
||||
})
|
||||
.from(tenantSchema.agent)
|
||||
|
||||
@@ -98,7 +98,13 @@ export class TenantAdminService {
|
||||
const urlParts = env.DATABASE_URL?.split("/") ?? [];
|
||||
urlParts.pop();
|
||||
|
||||
const newTenant: InsertTenant = { ...request, longName: "", databaseUrl: "" };
|
||||
const newTenant: InsertTenant = {
|
||||
...request,
|
||||
longName: "",
|
||||
databaseUrl: "",
|
||||
descriptions: { en: "" },
|
||||
languages: ["en"],
|
||||
};
|
||||
newTenant.databaseUrl = urlParts.join("/") + "/" + newTenant.shortName;
|
||||
|
||||
try {
|
||||
@@ -231,10 +237,12 @@ export class TenantAdminService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tenant data (longName, shortName, description, logo)
|
||||
* Update tenant data (longName, shortName, descriptions, languages, logo)
|
||||
*/
|
||||
async updateTenantData(
|
||||
updateData: Partial<Pick<InsertTenant, "longName" | "shortName" | "description" | "logo">>,
|
||||
updateData: Partial<
|
||||
Pick<InsertTenant, "longName" | "shortName" | "descriptions" | "languages" | "logo">
|
||||
>,
|
||||
) {
|
||||
const log = logger.setContext("TenantAdminService");
|
||||
log.debug("Updating tenant data", {
|
||||
|
||||
@@ -41,7 +41,8 @@ function createSystemTenant(): SelectTenant {
|
||||
id: "system",
|
||||
shortName: "open-reception",
|
||||
longName: "Open Reception",
|
||||
description: "Secure appointment booking platform",
|
||||
descriptions: { en: "Secure appointment booking platform" },
|
||||
languages: ["en"],
|
||||
logo: null,
|
||||
databaseUrl: "",
|
||||
setupState: "FIRST_CHANNEL_CREATED",
|
||||
|
||||
@@ -52,6 +52,14 @@ function createAuthStore() {
|
||||
unsubscribe();
|
||||
return authState!.isAuthenticated;
|
||||
},
|
||||
getTenant: () => {
|
||||
let authState: AuthState;
|
||||
const unsubscribe = store.subscribe((state) => {
|
||||
authState = state;
|
||||
});
|
||||
unsubscribe();
|
||||
return authState!.user?.tenantId || null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { toast } from "svelte-sonner";
|
||||
import { writable } from "svelte/store";
|
||||
import { auth } from "./auth";
|
||||
import { m } from "$i18n/messages";
|
||||
import { goto } from "$app/navigation";
|
||||
import { ROUTES } from "$lib/const/routes";
|
||||
|
||||
const log = logger.setContext("TenantsStore");
|
||||
|
||||
@@ -26,8 +28,9 @@ const createTenantsStore = () => {
|
||||
init: (tenants: TTenant[]) => {
|
||||
store.set({ tenants, isLoading: false, currentTenant: null });
|
||||
},
|
||||
setCurrentTenant: (tenantId: string | null, updateApi: boolean = true) => {
|
||||
if (updateApi) {
|
||||
setCurrentTenant: (tenantId: string | null) => {
|
||||
const curTenant = auth.getTenant();
|
||||
if (tenantId !== curTenant) {
|
||||
const success = changeTenantUsingApi(tenantId);
|
||||
if (!success) {
|
||||
log.error("Failed to change tenant via API", { tenantId });
|
||||
@@ -35,12 +38,14 @@ const createTenantsStore = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
store.update((state) => {
|
||||
const currentTenant = state.tenants.find((t) => t.id === tenantId) || null;
|
||||
auth.setTenantId(tenantId);
|
||||
return { ...state, currentTenant };
|
||||
});
|
||||
if (tenantId !== curTenant) {
|
||||
goto(ROUTES.DASHBOARD.MAIN);
|
||||
}
|
||||
},
|
||||
reload: async () => {
|
||||
store.update((state) => {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { SelectTenant } from "$lib/server/db/central-schema";
|
||||
|
||||
export type TTenant = Pick<SelectTenant, "id" | "shortName" | "longName" | "setupState">;
|
||||
export type TTenant = Pick<SelectTenant, "id" | "shortName" | "languages" | "setupState">;
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
if (data?.tenants) {
|
||||
tenants.init(await data.tenants);
|
||||
if ($auth.user?.tenantId) {
|
||||
tenants.setCurrentTenant($auth.user.tenantId, false);
|
||||
tenants.setCurrentTenant($auth.user.tenantId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+48
-30
@@ -2,30 +2,44 @@
|
||||
import { m } from "$i18n/messages.js";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { type Infer, superForm, type SuperValidated } from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import ItemIcon from "@lucide/svelte/icons/user-star";
|
||||
import { formSchema, type FormSchema } from ".";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob";
|
||||
import { LanguageTabs } from "$lib/components/ui/language-tabs";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import ItemIcon from "@lucide/svelte/icons/user-star";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from ".";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
let { data, done }: { done: () => void; data: { form: SuperValidated<Infer<FormSchema>> } } =
|
||||
$props();
|
||||
let { done }: { done: () => void } = $props();
|
||||
|
||||
const form = superForm(data.form, {
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["agents.add.success"]());
|
||||
done();
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["agents.add.errors.unknown"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
const tenantLocales = get(tenants).currentTenant?.languages ?? [];
|
||||
const form = superForm(
|
||||
{
|
||||
name: "",
|
||||
descriptions: tenantLocales.reduce(
|
||||
(acc, locale) => ({ ...acc, [locale]: "" }),
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
image: "",
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
});
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
toast.success(m["agents.add.success"]());
|
||||
done();
|
||||
} else if (event.result.type === "failure") {
|
||||
toast.error(m["agents.add.errors.unknown"]());
|
||||
}
|
||||
isSubmitting = false;
|
||||
},
|
||||
onSubmit: () => (isSubmitting = true),
|
||||
},
|
||||
);
|
||||
|
||||
let isSubmitting = $state(false);
|
||||
|
||||
@@ -42,16 +56,20 @@
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="description">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["agents.add.fields.description.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.description} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>{m["agents.add.fields.description.description"]()}</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<LanguageTabs>
|
||||
{#snippet children({ locale })}
|
||||
<Form.Field {form} name={`descriptions.${locale}`}>
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["agents.add.fields.description.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.descriptions[locale]} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>{m["agents.add.fields.description.description"]()}</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/snippet}
|
||||
</LanguageTabs>
|
||||
<Form.Field {form} name="image">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const formSchema = z.object({
|
||||
.string()
|
||||
.min(2, m["agents.add.fields.name.errors.length"]())
|
||||
.max(50, m["agents.add.fields.name.errors.length"]()),
|
||||
description: z.string().optional().or(z.literal("")),
|
||||
descriptions: z.record(z.string(), z.string()).optional(),
|
||||
image: z.string().optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
|
||||
+26
-14
@@ -2,25 +2,33 @@
|
||||
import { m } from "$i18n/messages.js";
|
||||
import * as Form from "$lib/components/ui/form";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob";
|
||||
import { LanguageTabs } from "$lib/components/ui/language-tabs";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import type { TAgent } from "$lib/types/agent";
|
||||
import ItemIcon from "@lucide/svelte/icons/user-star";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { superForm } from "sveltekit-superforms";
|
||||
import { zodClient } from "sveltekit-superforms/adapters";
|
||||
import { formSchema } from ".";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { InputCroppedImageBlob } from "$lib/components/ui/input-cropped-image-blob";
|
||||
import ItemIcon from "@lucide/svelte/icons/user-star";
|
||||
import { tenants } from "$lib/stores/tenants";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
let { entity, done }: { entity: TAgent; done: () => void } = $props();
|
||||
|
||||
const tenantLocales = get(tenants).currentTenant?.languages ?? [];
|
||||
const form = superForm(
|
||||
{
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
descriptions: tenantLocales.reduce(
|
||||
(acc, locale) => ({ ...acc, [locale]: entity.descriptions[locale] ?? "" }),
|
||||
{} as { [key: string]: string },
|
||||
),
|
||||
image: entity.image ?? "",
|
||||
},
|
||||
{
|
||||
dataType: "json",
|
||||
validators: zodClient(formSchema),
|
||||
onResult: async (event) => {
|
||||
if (event.result.type === "success") {
|
||||
@@ -50,16 +58,20 @@
|
||||
</Form.Control>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<Form.Field {form} name="description">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["agents.add.fields.description.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.description} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>{m["agents.add.fields.description.description"]()}</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
<LanguageTabs>
|
||||
{#snippet children({ locale })}
|
||||
<Form.Field {form} name={`descriptions.${locale}`}>
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<Form.Label>{m["agents.add.fields.description.title"]()}</Form.Label>
|
||||
<Textarea {...props} bind:value={$formData.descriptions[locale]} maxlength={200} />
|
||||
{/snippet}
|
||||
</Form.Control>
|
||||
<Form.Description>{m["agents.add.fields.description.description"]()}</Form.Description>
|
||||
<Form.FieldErrors />
|
||||
</Form.Field>
|
||||
{/snippet}
|
||||
</LanguageTabs>
|
||||
<Form.Field {form} name="image">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const formSchema = z.object({
|
||||
.string()
|
||||
.min(2, m["agents.add.fields.name.errors.length"]())
|
||||
.max(50, m["agents.add.fields.name.errors.length"]()),
|
||||
description: z.string().optional().or(z.literal("")),
|
||||
descriptions: z.record(z.string(), z.string()).optional(),
|
||||
image: z.string().optional().or(z.literal("")),
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export const load = async (event) => {
|
||||
});
|
||||
|
||||
return {
|
||||
form: await superValidate(zod(addFormSchema)),
|
||||
streamed: {
|
||||
list,
|
||||
},
|
||||
@@ -71,7 +70,7 @@ export const actions: Actions = {
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
name: form.data.name,
|
||||
description: form.data.description,
|
||||
descriptions: form.data.descriptions,
|
||||
image: form.data.image,
|
||||
}),
|
||||
});
|
||||
@@ -118,7 +117,7 @@ export const actions: Actions = {
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
name: form.data.name,
|
||||
description: form.data.description,
|
||||
descriptions: form.data.descriptions,
|
||||
image: form.data.image ?? null,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
<PlusIcon /> {m["agents.add.title"]()}
|
||||
{/snippet}
|
||||
<AddAgentForm
|
||||
{data}
|
||||
done={() => {
|
||||
invalidate(ROUTES.DASHBOARD.AGENTS);
|
||||
closeDialog("add");
|
||||
@@ -69,7 +68,7 @@
|
||||
<List>
|
||||
{#each items as item (item.id)}
|
||||
<ListItem
|
||||
image={item.image ?? UnknownItemIcon}
|
||||
image={item.image || UnknownItemIcon}
|
||||
title={item.name}
|
||||
actions={[
|
||||
{
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
type: "action",
|
||||
icon: SelectIcon,
|
||||
label: m["select"](),
|
||||
onClick: () => tenantsStore.setCurrentTenant(item.id, true),
|
||||
onClick: () => tenantsStore.setCurrentTenant(item.id),
|
||||
},
|
||||
{
|
||||
type: "divider",
|
||||
|
||||
@@ -32,13 +32,13 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
// Verify user is authenticated and is global admin
|
||||
checkPermission(locals, null, true);
|
||||
|
||||
const body = await request.json();
|
||||
const validation = tenantSwitchSchema.safeParse(body);
|
||||
|
||||
if (!locals.user?.sessionId) {
|
||||
throw new ValidationError(ERRORS.SECURITY.SESSION_MISSING);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const validation = tenantSwitchSchema.safeParse(body);
|
||||
|
||||
if (!validation.success) {
|
||||
logger.warn("Invalid tenant switch request", {
|
||||
userId: locals.user?.id,
|
||||
|
||||
@@ -211,7 +211,7 @@ export const GET: RequestHandler = async ({ locals }) => {
|
||||
.select({
|
||||
id: tenant.id,
|
||||
shortName: tenant.shortName,
|
||||
longName: tenant.longName,
|
||||
languages: tenant.languages,
|
||||
setupState: tenant.setupState,
|
||||
logo: tenant.logo,
|
||||
})
|
||||
|
||||
@@ -36,15 +36,15 @@ registerOpenAPIRoute("/tenants/{id}/agents", "POST", {
|
||||
description: "Agent name",
|
||||
example: "Support Agent",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Agent description",
|
||||
example: "Handles customer support requests",
|
||||
descriptions: {
|
||||
type: "object",
|
||||
description: "Localized agent descriptions. Keys are locale codes.",
|
||||
additionalProperties: true,
|
||||
example: { en: "Support Agent", de: "Service-Mitarbeiterin" },
|
||||
},
|
||||
logo: {
|
||||
image: {
|
||||
type: "string",
|
||||
format: "byte",
|
||||
description: "Base64 encoded agent logo",
|
||||
description: "Base64 encoded agent image",
|
||||
},
|
||||
},
|
||||
required: ["name"],
|
||||
@@ -66,8 +66,8 @@ registerOpenAPIRoute("/tenants/{id}/agents", "POST", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Agent ID" },
|
||||
name: { type: "string", description: "Agent name" },
|
||||
description: { type: "string", description: "Agent description" },
|
||||
logo: { type: "string", format: "byte", description: "Agent logo" },
|
||||
descriptions: { type: "object", description: "Localized Agent descriptions" },
|
||||
image: { type: "string", description: "Agent image" },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
@@ -150,8 +150,8 @@ registerOpenAPIRoute("/tenants/{id}/agents", "GET", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Agent ID" },
|
||||
name: { type: "string", description: "Agent name" },
|
||||
description: { type: "string", description: "Agent description" },
|
||||
logo: { type: "string", format: "byte", description: "Agent logo" },
|
||||
descriptions: { type: "object", description: "Localized Agent descriptions" },
|
||||
image: { type: "string", description: "Agent image" },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
|
||||
@@ -48,8 +48,8 @@ registerOpenAPIRoute("/tenants/{id}/agents/{agentId}", "GET", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Agent ID" },
|
||||
name: { type: "string", description: "Agent name" },
|
||||
description: { type: "string", description: "Agent description" },
|
||||
logo: { type: "string", format: "byte", description: "Agent logo" },
|
||||
descriptions: { type: "object", description: "Localized Agent descriptions" },
|
||||
image: { type: "string", description: "Agent image" },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
@@ -130,15 +130,15 @@ registerOpenAPIRoute("/tenants/{id}/agents/{agentId}", "PUT", {
|
||||
description: "Agent name",
|
||||
example: "Updated Support Agent",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
description: "Agent description",
|
||||
example: "Updated description for customer support",
|
||||
descriptions: {
|
||||
type: "object",
|
||||
description: "Localized agent descriptions. Keys are locale codes.",
|
||||
additionalProperties: true,
|
||||
example: { en: "Support Agent", de: "Service-Mitarbeiterin" },
|
||||
},
|
||||
logo: {
|
||||
image: {
|
||||
type: "string",
|
||||
format: "byte",
|
||||
description: "Base64 encoded agent logo",
|
||||
description: "Base64 encoded agent image",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -159,8 +159,8 @@ registerOpenAPIRoute("/tenants/{id}/agents/{agentId}", "PUT", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Agent ID" },
|
||||
name: { type: "string", description: "Agent name" },
|
||||
description: { type: "string", description: "Agent description" },
|
||||
logo: { type: "string", format: "byte", description: "Agent logo" },
|
||||
descriptions: { type: "string", description: "Localized Agent descriptions" },
|
||||
image: { type: "string", description: "Agent image" },
|
||||
},
|
||||
required: ["id", "name"],
|
||||
},
|
||||
|
||||
@@ -30,10 +30,10 @@ registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
|
||||
type: "object",
|
||||
properties: {
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1, maxLength: 100 },
|
||||
description: "Channel names (one per language)",
|
||||
example: ["Support", "Unterstützung"],
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
description: "Localized channel names. Keys are locale codes.",
|
||||
example: { en: "Support", de: "Unterstützung" },
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
@@ -41,16 +41,10 @@ registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
|
||||
example: "#FF0000",
|
||||
},
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions (optional, one per language)",
|
||||
example: ["Customer support channel", "Kundensupport-Kanal"],
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 2, maxLength: 5 },
|
||||
description: "Supported languages",
|
||||
example: ["en", "de"],
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
description: "Localized channel descriptions. Keys are locale codes.",
|
||||
example: { en: "Some description", de: "Eine Beschreibung" },
|
||||
},
|
||||
isPublic: {
|
||||
type: "boolean",
|
||||
@@ -107,17 +101,11 @@ registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Channel ID" },
|
||||
names: { type: "array", items: { type: "string" }, description: "Channel names" },
|
||||
names: { type: "object", description: "Localized Channel names" },
|
||||
color: { type: "string", description: "Channel color" },
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions",
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Supported languages",
|
||||
type: "object",
|
||||
description: "Localized Channel descriptions",
|
||||
},
|
||||
isPublic: { type: "boolean", description: "Public visibility" },
|
||||
requiresConfirmation: { type: "boolean", description: "Requires confirmation" },
|
||||
@@ -128,8 +116,8 @@ registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
logo: { type: "string", format: "byte" },
|
||||
descriptions: { type: "object" },
|
||||
image: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -147,7 +135,7 @@ registerOpenAPIRoute("/tenants/{id}/channels", "POST", {
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "names", "languages", "agents", "slotTemplates"],
|
||||
required: ["id", "names", "agents", "slotTemplates"],
|
||||
},
|
||||
},
|
||||
required: ["message", "channel"],
|
||||
@@ -227,21 +215,11 @@ registerOpenAPIRoute("/tenants/{id}/channels", "GET", {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Channel ID" },
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel names",
|
||||
},
|
||||
names: { type: "object", description: "Localized Channel names" },
|
||||
color: { type: "string", description: "Channel color" },
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions",
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Supported languages",
|
||||
type: "object",
|
||||
description: "Localized Channel descriptions",
|
||||
},
|
||||
isPublic: { type: "boolean", description: "Public visibility" },
|
||||
requiresConfirmation: { type: "boolean", description: "Requires confirmation" },
|
||||
@@ -252,8 +230,8 @@ registerOpenAPIRoute("/tenants/{id}/channels", "GET", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
logo: { type: "string", format: "byte" },
|
||||
descriptions: { type: "object" },
|
||||
image: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -271,7 +249,7 @@ registerOpenAPIRoute("/tenants/{id}/channels", "GET", {
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "names", "languages", "agents", "slotTemplates"],
|
||||
required: ["id", "names", "agents", "slotTemplates"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,17 +47,11 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "GET", {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Channel ID" },
|
||||
names: { type: "array", items: { type: "string" }, description: "Channel names" },
|
||||
names: { type: "object", description: "Localized Channel names" },
|
||||
color: { type: "string", description: "Channel color" },
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions",
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Supported languages",
|
||||
type: "object",
|
||||
description: "Localized Channel descriptions",
|
||||
},
|
||||
isPublic: { type: "boolean", description: "Public visibility" },
|
||||
requiresConfirmation: { type: "boolean", description: "Requires confirmation" },
|
||||
@@ -68,8 +62,8 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "GET", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
logo: { type: "string", format: "byte" },
|
||||
descriptions: { type: "object" },
|
||||
image: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,7 +81,7 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "GET", {
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "names", "languages", "agents", "slotTemplates"],
|
||||
required: ["id", "names", "agents", "slotTemplates"],
|
||||
},
|
||||
},
|
||||
required: ["channel"],
|
||||
@@ -160,10 +154,10 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "PUT", {
|
||||
type: "object",
|
||||
properties: {
|
||||
names: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 1, maxLength: 100 },
|
||||
description: "Channel names (one per language)",
|
||||
example: ["Updated Support", "Aktualisierte Unterstützung"],
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
description: "Localized channel names. Keys are locale codes.",
|
||||
example: { en: "Support", de: "Unterstützung" },
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
@@ -171,16 +165,10 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "PUT", {
|
||||
example: "#00FF00",
|
||||
},
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions (one per language)",
|
||||
example: ["Updated customer support channel", "Aktualisierter Kundensupport-Kanal"],
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string", minLength: 2, maxLength: 5 },
|
||||
description: "Supported languages",
|
||||
example: ["en", "de"],
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
description: "Localized channel descriptions. Keys are locale codes.",
|
||||
example: { en: "Some description", de: "Eine Beschreibung" },
|
||||
},
|
||||
isPublic: {
|
||||
type: "boolean",
|
||||
@@ -241,17 +229,11 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "PUT", {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid", description: "Channel ID" },
|
||||
names: { type: "array", items: { type: "string" }, description: "Channel names" },
|
||||
names: { type: "object", description: "Localized Channel names" },
|
||||
color: { type: "string", description: "Channel color" },
|
||||
descriptions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Channel descriptions",
|
||||
},
|
||||
languages: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Supported languages",
|
||||
type: "object",
|
||||
description: "Localized Channel descriptions",
|
||||
},
|
||||
isPublic: { type: "boolean", description: "Public visibility" },
|
||||
requiresConfirmation: { type: "boolean", description: "Requires confirmation" },
|
||||
@@ -262,8 +244,8 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "PUT", {
|
||||
properties: {
|
||||
id: { type: "string", format: "uuid" },
|
||||
name: { type: "string" },
|
||||
description: { type: "string" },
|
||||
logo: { type: "string", format: "byte" },
|
||||
descriptions: { type: "object" },
|
||||
image: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -281,7 +263,7 @@ registerOpenAPIRoute("/tenants/{id}/channels/{channelId}", "PUT", {
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["id", "names", "languages", "agents", "slotTemplates"],
|
||||
required: ["id", "names", "agents", "slotTemplates"],
|
||||
},
|
||||
},
|
||||
required: ["message", "channel"],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "agent" RENAME COLUMN "description" TO "descriptions";--> statement-breakpoint
|
||||
ALTER TABLE "channel" ALTER COLUMN "descriptions" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "channel" DROP COLUMN "languages";
|
||||
@@ -0,0 +1,452 @@
|
||||
{
|
||||
"id": "71a71648-32b1-483e-b395-949a63f93f7d",
|
||||
"prevId": "b4773ec9-f77f-40ec-8da0-b6f3a4498ca7",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.agent": {
|
||||
"name": "agent",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"descriptions": {
|
||||
"name": "descriptions",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "varchar(250000)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.agent_absence": {
|
||||
"name": "agent_absence",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"start_date": {
|
||||
"name": "start_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"end_date": {
|
||||
"name": "end_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"absence_type": {
|
||||
"name": "absence_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"agent_absence_agent_id_agent_id_fk": {
|
||||
"name": "agent_absence_agent_id_agent_id_fk",
|
||||
"tableFrom": "agent_absence",
|
||||
"tableTo": "agent",
|
||||
"columnsFrom": ["agent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.appointment": {
|
||||
"name": "appointment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"client_id": {
|
||||
"name": "client_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"channel_id": {
|
||||
"name": "channel_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"appointment_date": {
|
||||
"name": "appointment_date",
|
||||
"type": "date",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expiry_date": {
|
||||
"name": "expiry_date",
|
||||
"type": "date",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "appointment_status",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'NEW'"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"phone": {
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"appointment_client_id_client_id_fk": {
|
||||
"name": "appointment_client_id_client_id_fk",
|
||||
"tableFrom": "appointment",
|
||||
"tableTo": "client",
|
||||
"columnsFrom": ["client_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"appointment_channel_id_channel_id_fk": {
|
||||
"name": "appointment_channel_id_channel_id_fk",
|
||||
"tableFrom": "appointment",
|
||||
"tableTo": "channel",
|
||||
"columnsFrom": ["channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.channel": {
|
||||
"name": "channel",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"names": {
|
||||
"name": "names",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"paused": {
|
||||
"name": "paused",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"descriptions": {
|
||||
"name": "descriptions",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"is_public": {
|
||||
"name": "is_public",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"requires_confirmation": {
|
||||
"name": "requires_confirmation",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.channel_agent": {
|
||||
"name": "channel_agent",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_id": {
|
||||
"name": "channel_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"channel_agent_channel_id_channel_id_fk": {
|
||||
"name": "channel_agent_channel_id_channel_id_fk",
|
||||
"tableFrom": "channel_agent",
|
||||
"tableTo": "channel",
|
||||
"columnsFrom": ["channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"channel_agent_agent_id_agent_id_fk": {
|
||||
"name": "channel_agent_agent_id_agent_id_fk",
|
||||
"tableFrom": "channel_agent",
|
||||
"tableTo": "agent",
|
||||
"columnsFrom": ["agent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.channel_slot_template": {
|
||||
"name": "channel_slot_template",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"channel_id": {
|
||||
"name": "channel_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slot_template_id": {
|
||||
"name": "slot_template_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"channel_slot_template_channel_id_channel_id_fk": {
|
||||
"name": "channel_slot_template_channel_id_channel_id_fk",
|
||||
"tableFrom": "channel_slot_template",
|
||||
"tableTo": "channel",
|
||||
"columnsFrom": ["channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"channel_slot_template_slot_template_id_slotTemplate_id_fk": {
|
||||
"name": "channel_slot_template_slot_template_id_slotTemplate_id_fk",
|
||||
"tableFrom": "channel_slot_template",
|
||||
"tableTo": "slotTemplate",
|
||||
"columnsFrom": ["slot_template_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.client": {
|
||||
"name": "client",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"hash_key": {
|
||||
"name": "hash_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"private_key_share": {
|
||||
"name": "private_key_share",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"client_hash_key_unique": {
|
||||
"name": "client_hash_key_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": ["hash_key"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.slotTemplate": {
|
||||
"name": "slotTemplate",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"weekdays": {
|
||||
"name": "weekdays",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"from": {
|
||||
"name": "from",
|
||||
"type": "time",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"to": {
|
||||
"name": "to",
|
||||
"type": "time",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.appointment_status": {
|
||||
"name": "appointment_status",
|
||||
"schema": "public",
|
||||
"values": ["NEW", "CONFIRMED", "HELD", "REJECTED", "NO_SHOW"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@
|
||||
"when": 1758555647097,
|
||||
"tag": "0002_typical_the_twelve",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1759144219626,
|
||||
"tag": "0003_big_boomerang",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user