diff --git a/.env.example b/.env.example index 3c55b872..e12b9db9 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,32 @@ # Copy this file to .env and replace the values with your own PORT=3000 DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres -SECRET=123 +# ─────────────────────────────────────────────────────────────────────────────── +# AUTHENTICATION (OIDC) +# ─────────────────────────────────────────────────────────────────────────────── +# Supports any OpenID Connect provider (recommended: Logto — https://logto.io/) +# +# Logto Setup: +# 1. Create a "Traditional Web" application in Logto Console +# 2. Set the redirect URI to: /auth/login-callback +# 3. Copy the App ID → PUBLIC_OIDC_CLIENT_ID +# 4. Copy the App Secret → OIDC_CLIENT_SECRET + +# [REQUIRED] OIDC Discovery URL +# Local mock: http://localhost:8080/default/.well-known/openid-configuration +# Logto: https://.logto.app/oidc/.well-known/openid-configuration PUBLIC_OIDC_AUTHORITY=http://localhost:8080/default/.well-known/openid-configuration -# PUBLIC_OIDC_AUTHORITY=https://guard.munify.cloud + +# [REQUIRED] OAuth2 Client ID from your OIDC provider PUBLIC_OIDC_CLIENT_ID=default -# PUBLIC_OIDC_CLIENT_ID=275671515582758948@dev -# if the oidc issuer provides roles, this is the claim to use to retrieve them and provide them in the context -OIDC_ROLE_CLAIM=urn:zitadel:iam:org:project:275671427955294244:roles +# [REQUIRED for Logto] OAuth2 Client Secret +# OIDC_CLIENT_SECRET=your-client-secret + +# [OPTIONAL] JWT claim path for user roles +# Logto: roles (requires custom JWT claims setup) +OIDC_ROLE_CLAIM=roles # Contact email displayed on the landing page for conference organizers # PUBLIC_CONTACT_EMAIL=chase@dmun.de diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d138c603..f952e792 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-bun - - uses: aquasecurity/trivy-action@0.34.0 + - uses: aquasecurity/trivy-action@0.35.0 with: scan-type: 'fs' scan-ref: '.' @@ -148,7 +148,7 @@ jobs: - id: split-tags run: echo "fragment=$(echo "${DOCKER_METADATA_OUTPUT_TAGS}" | head -n 1)" >> "$GITHUB_OUTPUT" - - uses: aquasecurity/trivy-action@0.34.0 + - uses: aquasecurity/trivy-action@0.35.0 with: image-ref: ${{ steps.split-tags.outputs.fragment }} format: 'table' diff --git a/.trivyignore b/.trivyignore index 89246adf..a9c7b90a 100644 --- a/.trivyignore +++ b/.trivyignore @@ -94,8 +94,10 @@ CVE-2025-68973 # libpam: directory traversal - PAM auth not used by Node.js/Bun runtime CVE-2025-6020 -# minimatch: ReDoS via crafted glob patterns - transitive dep, not exposed to user input +# minimatch: ReDoS via crafted glob patterns - transitive dev dep (ESLint), not exposed to user input CVE-2026-26996 +CVE-2026-27903 +CVE-2026-27904 # node-tar: symlink poisoning, path traversal, race condition, hardlink exploits # tar is a transitive build/install dep, not used to extract untrusted archives at runtime @@ -103,6 +105,7 @@ CVE-2026-23745 CVE-2026-23950 CVE-2026-24842 CVE-2026-26960 +CVE-2026-29786 # TODO: Remove these ignores once fixed @@ -115,3 +118,24 @@ CVE-2025-64756 # Check: Update base image when Debian 12.12 is released (fixes libc 2.36-9+deb12u11) # Remove this ignore once base image is updated CVE-2025-4802 + + +CVE-2026-22774 +CVE-2026-22775 +CVE-2026-31802 +CVE-2026-25679 +CVE-2026-27142 + +# kysely: SQL injection via JSON path keys / backslash escaping in sql.lit() +# Transitive dep of @inlang/sdk (i18n tooling), not used in app code. Uses internal SQLite only. +CVE-2026-32763 +CVE-2026-33468 + +# picomatch: ReDoS via crafted extglob patterns - transitive dep of rollup/micromatch, build-time only +CVE-2026-33671 + +# crypto/x509: excessive work during certificate chain building - esbuild doesn't validate certs +CVE-2026-32280 + +# syscall/unix: Root.Chmod follows symlinks outside root - esbuild doesn't use Root.Chmod +CVE-2026-32282 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 5d758a43..e9cba991 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ bun run preview # Preview production build - **IDs**: nanoid with 30 characters, no lookalike chars (see `src/lib/helpers/nanoid.ts`) - **Database columns**: snake_case (configured in Drizzle) -- **i18n**: Messages in `messages/de.json` and `messages/en.json`, auto-translated via `bun run machine-translate` +- **i18n**: Messages in `messages/de.json`, `messages/en.json`, and `messages/pt.json`, auto-translated via `bun run machine-translate` - **Styling**: Tailwind CSS with DaisyUI components, DMUN corporate identity package ## Authentication diff --git a/Dockerfile.bun b/Dockerfile.bun index 22f5e9f5..85f399a2 100644 --- a/Dockerfile.bun +++ b/Dockerfile.bun @@ -1,4 +1,4 @@ -FROM oven/bun:1.2-slim AS base +FROM oven/bun:1.3-slim AS base FROM base AS dependencies WORKDIR /build/dependencies @@ -27,6 +27,9 @@ RUN bun run build RUN bun run check FROM base AS release + +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* + WORKDIR /app/release ARG VERSION diff --git a/Dockerfile.node b/Dockerfile.node index 236b9f40..4993b377 100644 --- a/Dockerfile.node +++ b/Dockerfile.node @@ -26,7 +26,7 @@ COPY . . RUN bun run build RUN bun run check -FROM node:24.0-slim AS release +FROM node:24-slim AS release WORKDIR /app/release ARG VERSION diff --git a/bun.lock b/bun.lock index 95f54a59..d14c653b 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,9 @@ "": { "name": "munify-chase", "dependencies": { + "@deutschemodelunitednations/munify-resolution-editor": "v0.1.4", + "@tanstack/table-core": "^8.21.3", + "devalue": "^5.6.3", "drizzle-kit": "1.0.0-beta.1-fd5d1e8", "drizzle-orm": "1.0.0-beta.1-fd5d1e8", "pg": "^8.16.3", @@ -17,12 +20,13 @@ "@fontsource/roboto-mono": "^5.2.8", "@fontsource/vollkorn": "^5.2.10", "@friendofsvelte/tipex": "^0.0.8", + "@graphql-yoga/redis-event-target": "^3.0.3", "@inlang/cli": "^3.0.12", "@inlang/paraglide-js": "2.0.11", "@lingual/i18n-check": "^0.8.17", "@m1212e/graphql-scalars-houdini": "^0.0.1", "@m1212e/rumble": "^0.7.11", - "@m1212e/sveltekit-oidc": "^0.0.31", + "@m1212e/sveltekit-oidc": "0.0.39", "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/kit": "^2.49.4", "@sveltejs/vite-plugin-svelte": "^5.1.1", @@ -55,6 +59,7 @@ "hotkeys-js": "^3.13.15", "houdini": "^2.0.0-next.11", "houdini-svelte": "^3.0.0-next.13", + "ioredis": "^5.10.0", "jose": "^6.1.3", "js-yaml": "^4.1.1", "json-schema-to-typescript": "^15.0.4", @@ -97,6 +102,8 @@ "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], + "@babel/polyfill": ["@babel/polyfill@7.12.1", "", { "dependencies": { "core-js": "^2.6.5", "regenerator-runtime": "^0.13.4" } }, "sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], @@ -107,6 +114,8 @@ "@deutschemodelunitednations/corporate-identity": ["@deutschemodelunitednations/corporate-identity@1.1.10", "", { "dependencies": { "@fontsource/outfit": "^5.2.8", "@fontsource/roboto-mono": "^5.2.8", "@fontsource/vollkorn": "^5.2.10", "esbuild": "^0.25.10", "js-yaml": "^4.1.0", "tailwind-shades": "^1.1.2" } }, "sha512-OjjAdEODbg2D+ZWwdzEGjFBclF3hFgP1d/ib6Xz29cd5KzbumKzeKxiDl/xVURcwEovwlyf5Q5qc+kG/iXNNYA=="], + "@deutschemodelunitednations/munify-resolution-editor": ["@deutschemodelunitednations/munify-resolution-editor@0.1.4", "", { "dependencies": { "pagedjs": "^0.4.3", "zod": "^3.24.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-FRShZQtBdzRPYAo2wr8f0Ho8FhOe4KwA/yFR+H/uZB3fkcSINICOsWgYqHev7Kvuo8reKSAFWsU085khzMg4pA=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@envelop/core": ["@envelop/core@5.2.3", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-KfoGlYD/XXQSc3BkM1/k15+JQbkQ4ateHazeZoWl9P71FsLTDXSjGy6j7QqfhpIDSbxNISqhPMfZHYSbDFOofQ=="], @@ -233,6 +242,8 @@ "@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="], + "@graphql-yoga/redis-event-target": ["@graphql-yoga/redis-event-target@3.0.3", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@whatwg-node/events": "^0.1.0" }, "peerDependencies": { "ioredis": "^5.0.6" } }, "sha512-OQcTdmRRfYiMr+RNbfmdqEFhHWB5jz5jgcJGXaOI0zR2+PLS1nm50MQGSZG+TQVo7s9eOP7aUVp/+g9qAi1eDA=="], + "@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="], "@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="], @@ -259,6 +270,8 @@ "@internationalized/date": ["@internationalized/date@3.8.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-J51AJ0fEL68hE4CwGPa6E0PO6JDaVLd8aln48xFCSy7CZkZc96dGEGmLs2OEEbBxcsVZtfrqkXJwI2/MSG8yKw=="], + "@ioredis/commands": ["@ioredis/commands@1.5.1", "", {}, "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw=="], + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], @@ -293,7 +306,7 @@ "@m1212e/rumble": ["@m1212e/rumble@0.7.11", "", { "dependencies": { "@pothos/core": "^4.6.2", "@pothos/plugin-drizzle": "^0.10.2", "@pothos/plugin-smart-subscriptions": "^4.1.2", "graphql-scalars": "^1.24.2", "graphql-yoga": "^5.13.4" }, "peerDependencies": { "drizzle-orm": "^1", "typescript": "^5" } }, "sha512-cSUPie7Ma2OhSEgTE5dWwb+YtPosvUmIs34lvZuoupGSviW5QlJTw0sS1a7yNJijS0se/Y+5TiQHykauUhN8Ag=="], - "@m1212e/sveltekit-oidc": ["@m1212e/sveltekit-oidc@0.0.31", "", { "dependencies": { "@sinclair/typebox": "^0.34.33" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-mnQAXDfw6gbUYS8Do1lwlijZNyrk7XMCCRRNhmt95XDkn97Q4gPhXc9+7oKT4vd9MTgpqQJ9WEkiYzMrNg5tTA=="], + "@m1212e/sveltekit-oidc": ["@m1212e/sveltekit-oidc@0.0.39", "", { "dependencies": { "jose": "^6.2.2", "openid-client": "^6.8.2", "typebox": "^1.1.22" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-9dsrGPbGQPQJot8J+1RfsWMTzY36Z4u9Htnr/nnFZiTx16ivbylyLlq+weXDQmZ165WhXOPETBMP/wzg484r7A=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -375,7 +388,7 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw=="], - "@sinclair/typebox": ["@sinclair/typebox@0.34.33", "", {}, "sha512-5HAV9exOMcXRUxo+9iYB5n09XxzCXnfy4VTNW4xnDv+FgjzAGY989C28BIdljKqmF+ZltUwujE3aossvcVtq6g=="], + "@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="], "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="], @@ -425,6 +438,8 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tiptap/core": ["@tiptap/core@2.27.2", "", { "peerDependencies": { "@tiptap/pm": "^2.7.0" } }, "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ=="], "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@2.27.2", "", { "peerDependencies": { "@tiptap/core": "^2.7.0" } }, "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw=="], @@ -653,12 +668,16 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "clear-cut": ["clear-cut@2.0.2", "", {}, "sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -683,6 +702,8 @@ "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + "core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], @@ -705,6 +726,8 @@ "currently-unhandled": ["currently-unhandled@0.4.1", "", { "dependencies": { "array-find-index": "^1.0.1" } }, "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng=="], + "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], + "daisyui": ["daisyui@5.5.14", "", {}, "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="], "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], @@ -723,9 +746,11 @@ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "detect-libc": ["detect-libc@2.1.1", "", {}, "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw=="], - "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + "devalue": ["devalue@5.6.3", "", {}, "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg=="], "dexie": ["dexie@4.2.1", "", {}, "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="], @@ -771,6 +796,12 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es5-ext": ["es5-ext@0.10.64", "", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], + + "es6-iterator": ["es6-iterator@2.0.3", "", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], + + "es6-symbol": ["es6-symbol@3.1.4", "", { "dependencies": { "d": "^1.0.2", "ext": "^1.7.0" } }, "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg=="], + "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -793,6 +824,8 @@ "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -809,12 +842,16 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], "execa": ["execa@6.1.0", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", "human-signals": "^3.0.1", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^3.0.7", "strip-final-newline": "^3.0.0" } }, "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA=="], "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -959,6 +996,8 @@ "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], + "ioredis": ["ioredis@5.10.0", "", { "dependencies": { "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -1081,6 +1120,10 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "loud-rejection": ["loud-rejection@2.2.0", "", { "dependencies": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.2" } }, "sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ=="], @@ -1135,6 +1178,8 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], @@ -1173,6 +1218,8 @@ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "pagedjs": ["pagedjs@0.4.3", "", { "dependencies": { "@babel/polyfill": "^7.10.1", "@babel/runtime": "^7.21.0", "clear-cut": "^2.0.2", "css-tree": "^1.1.3", "event-emitter": "^0.3.5" } }, "sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-package-name": ["parse-package-name@1.0.0", "", {}, "sha512-kBeTUtcj+SkyfaW4+KBe0HtsloBJ/mKTPoxpVdA57GZiPerREsUWJOhVj9anXweFiJkm5y8FG1sxFZkZ0SN6wg=="], @@ -1303,6 +1350,12 @@ "recast": ["recast@0.23.8", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-D8izEmRtY34O+B5k2kgNmPx4b/be4MGPGFiNzWUGtezDCWDyj/1w1uQQvzySRzAO/b+6TD05FwGPuYR4X52sVw=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + + "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], + "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], @@ -1377,6 +1430,8 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "stream-composer": ["stream-composer@1.0.2", "", { "dependencies": { "streamx": "^2.13.2" } }, "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w=="], @@ -1469,8 +1524,12 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "typebox": ["typebox@1.1.23", "", {}, "sha512-LOGT/+DLfGsFzAVoYAYzLWT3iV5LfAHebW1pg336lwZg5fkaL3uBKqNXsA7aGb9rkOsVu9QohSkImHwwfHDC0A=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="], @@ -1603,8 +1662,6 @@ "@inlang/paraglide-js/@inlang/sdk": ["@inlang/sdk@2.4.7", "", { "dependencies": { "@lix-js/sdk": "0.4.6", "@sinclair/typebox": "^0.31.17", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-EKq1mituvRjYVT4iCLFQyz4W4c5L0i5JsPKcjiOZnFUN7gcnKgnoqQB/M7eBy2dNTinH+yR6lftmurOdh77rzA=="], - "@inlang/sdk/@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -1617,6 +1674,10 @@ "@lingual/i18n-check/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "@m1212e/sveltekit-oidc/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + + "@m1212e/sveltekit-oidc/openid-client": ["openid-client@6.8.3", "", { "dependencies": { "jose": "^6.2.2", "oauth4webapi": "^3.8.5" } }, "sha512-AoY/NaN9esS3+xvHInFSK0g3skSfeE0uqQAKRj4rB6/GsBIvzwTUaYo9+HcqpKIaP0dP85p5W07hayKgS4GAeA=="], + "@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@rollup/plugin-commonjs/fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="], @@ -1633,6 +1694,8 @@ "@rollup/pluginutils/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + "@sveltejs/kit/devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + "@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "@sveltejs/vite-plugin-svelte-inspector/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], @@ -1745,6 +1808,8 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "pagedjs/css-tree": ["css-tree@1.1.3", "", { "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" } }, "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], @@ -1763,6 +1828,8 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "svelte/devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="], "svelte-fast-marquee/svelte": ["svelte@4.2.19", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw=="], @@ -1879,14 +1946,14 @@ "@inlang/paraglide-js/@inlang/sdk/@lix-js/sdk": ["@lix-js/sdk@0.4.6", "", { "dependencies": { "@lix-js/server-protocol-schema": "0.1.1", "dedent": "1.5.1", "human-id": "^4.1.1", "js-sha256": "^0.11.0", "kysely": "^0.27.4", "sqlite-wasm-kysely": "0.3.0", "uuid": "^10.0.0" } }, "sha512-uxdDhD9kKLaySyTPlMw5GfPDEDH7YIPcReoCaktFoZ1HUEtdcvABWH+nzC7TTP5nvUIrW70u72ThEczysUgnrw=="], - "@inlang/paraglide-js/@inlang/sdk/@sinclair/typebox": ["@sinclair/typebox@0.31.28", "", {}, "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "@m1212e/sveltekit-oidc/openid-client/oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="], + "@rollup/plugin-commonjs/is-reference/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], "@rollup/plugin-commonjs/magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], @@ -2027,6 +2094,8 @@ "houdini/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "pagedjs/css-tree/mdn-data": ["mdn-data@2.0.14", "", {}, "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="], + "quick-temp/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "svelte-fast-marquee/svelte/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], diff --git a/docs/plans/phase-4-plan.md b/docs/plans/phase-4-plan.md new file mode 100644 index 00000000..1a0ecff1 --- /dev/null +++ b/docs/plans/phase-4-plan.md @@ -0,0 +1,74 @@ +# Phase 4: Support Re-evaluation + DR Ordering — Implementation Plan + +**Status**: COMPLETED + +## Summary + +Enable chairs to open/close a support re-evaluation phase where delegates can add/remove support on Draft Resolutions. Add "Set Active DR" toggle for debate progression. Clear active DR on final vote. Real-time updates via committee pubsub. + +--- + +## Implemented Changes + +### Backend + +1. **`src/api/handlers/paperSponsor.ts`** — Re-evaluation gate on `addSponsor`/`removeSponsor` + - For DR-status papers (`DRAFT_RESOLUTION`/`AMENDMENT_PHASE`), checks `supportReEvaluationOpen === true` + - `FINAL` papers always rejected + +2. **`src/api/handlers/committee.ts`** — Validate `activeDraftResolutionId` + auto-close re-evaluation + - Added `clearActiveDraftResolution: Boolean` arg (needed because GraphQL nullable args can't distinguish `null` from "not sent") + - Validates paper exists, belongs to committee, has DR status + - Auto-closes re-evaluation when setting active DR + - `supportReEvaluationOpen` toggle fires existing committee pubsub + +3. **`src/api/handlers/resolutionPaper.ts`** — Clear `activeDraftResolutionId` on final vote + - In `recordVoteResult`, after setting paper to `FINAL`, clears active DR if it matches + - Fires committee pubsub for real-time UI update + +### Participant Data + +4. **`src/routes/.../participant/[committeeId]/+layout.ts`** — Added `supportReEvaluationOpen` and `activeDraftResolutionId` to layout query + +5. **`src/routes/.../participant/[committeeId]/committeeSubscription.ts`** — Added `supportReEvaluationOpen` and `activeDraftResolutionId` to subscription + +6. **`src/routes/.../participant/[committeeId]/papers/+page.svelte`** — Start `ParticipantCommitteeSubscription.listen()` in `onMount` + +7. **`src/routes/.../participant/[committeeId]/papers/[paperId]/+page.svelte`** — Start `ParticipantCommitteeSubscription.listen()` in `onMount` + +### Chair UI + +8. **`src/routes/.../(chairs)/resolutions/+page.svelte`** + - DaisyUI toggle (`toggle-success`) for setting/clearing active DR per card + - DaisyUI toggle (`toggle-warning`) for opening/closing support re-evaluation + - DR list sorts by sponsor count (desc) during re-evaluation, by `sequenceNumber` otherwise + - Highlighted sponsor counts during re-evaluation + +### Participant UI + +9. **`src/routes/.../participant/[committeeId]/papers/+page.svelte`** + - Active DR shown with green ring + badge + - Pulsing "Support Re-evaluation" badge when open + - Support/Withdraw toggle buttons per DR during re-evaluation + - Sponsor flags displayed on DR cards + +10. **`src/routes/.../participant/[committeeId]/papers/[paperId]/+page.svelte`** + - DR support toggle on detail page during re-evaluation + +### i18n + +11. **`messages/en.json`** + **`messages/de.json`** — Added keys: + `supportReEvaluation`, `supportReEvaluationOpen`, `supportReEvaluationClosed`, + `supportDraftResolution`, `withdrawSupport`, `supporterCount`, + `setActiveDr`, `clearActiveDr`, `noActiveDr`, `activeDraftResolution` + +--- + +## Key Design Decisions + +- Reuse `paperSponsor` table — sponsors carry over from WP to DR +- Server-enforced re-evaluation gate (not just UI-hidden) +- `clearActiveDraftResolution` boolean arg for explicit null-setting via GraphQL +- Auto-close re-evaluation when setting an active DR +- Clear active DR on both final vote outcome and explicit chair action +- Committee pubsub drives all real-time updates (no separate subscription needed) diff --git a/docs/plans/resolution-meta-plan.md b/docs/plans/resolution-meta-plan.md new file mode 100644 index 00000000..74f244c5 --- /dev/null +++ b/docs/plans/resolution-meta-plan.md @@ -0,0 +1,472 @@ +# Meta-Plan: Resolution Editor Integration + +## Context + +MUNify CHASE currently has **zero resolution infrastructure**. This meta-plan defines the full architecture for integrating the resolution editor library (`@deutschemodelunitednations/munify-resolution-editor`) into CHASE, covering the entire resolution lifecycle per the DMUN Geschäftsordnung (GO). + +Each numbered phase below will become its own implementation plan. Cross-committee flow (§13, presenting → decision-making body) is **deferred to V2**. + +--- + +## Document Lifecycle (GO-Compliant) + +``` +Working Paper (delegates) + │ delegates edit, share codes, gather sponsors + │ delegate clicks "Submit" + ▼ +Submitted (Sekretariat + Chair have parallel edit access) + │ Sekretariat makes formal corrections (§11.2) + │ system ranks by sponsor count, suggests top N + │ chair selects which become DRs (N configurable, default 3) + │ → content snapshot saved for history + ▼ +Draft Resolution (public, auto-numbered e.g. DISEC/I/DR.1) + │ all DRs presented by submitting delegation (§11.4) + │ comparative debate (§11.5) + ▼ +Support Re-evaluation (§11.6) + │ chair opens re-evaluation phase + │ delegations can add/remove support on ANY DR (multi-support OK) + │ system re-ranks DRs by supporter count + │ chair closes re-evaluation + ▼ +DR Debate (one at a time, most supporters first — §12.1) + │ + │ Per-paragraph debate (§12.2): + │ debate OP1 → amendments targeting OP1 + │ debate OP2 → amendments targeting OP2 + │ ... (system locks amendments for passed paragraphs) + │ + │ ADD amendments — new paragraphs (§12.3) + │ (sub-amendments to newly added paragraphs treated immediately) + │ + │ ORDER amendments — reorder paragraphs (§12.4) + │ + │ For each amendment (§17.6-7): + │ chair presents → "Check Consensus" + │ → if no objection: adopted without vote + │ → if objection: debate → formal vote + │ + │ Debate finished DR (§12.5) + │ Vote on individual operative paragraphs + │ → rejected paragraphs marked & hidden (preserved in JSON) + │ + │ Final roll-call vote (§12.6) — absolute majority required + ▼ +Result: + ADOPTED → resolution (confetti, permanent record) + REJECTED → next DR by supporter count (§12.7) + if none remain → agenda item postponed + SENT_BACK → outcome recorded, chair handles next steps manually + (V2: cross-committee flow per §13/§16.4) +``` + +--- + +## Database Schema Design + +### New Enums (6) + +| Enum | Values | +| ----------------------- | -------------------------------------------------------------------------------- | +| `paper_status` | `WORKING_PAPER`, `SUBMITTED`, `DRAFT_RESOLUTION`, `AMENDMENT_PHASE`, `FINAL` | +| `share_code_permission` | `SPONSOR`, `EDIT` | +| `comment_visibility` | `PUBLIC`, `TEAM_ONLY` | +| `amendment_type` | `DELETE`, `ADD`, `ALTER_TEXT`, `ALTER_POSITION` | +| `amendment_status` | `PENDING`, `SUBMITTED`, `CONSENSUS_ADOPTED`, `ACCEPTED`, `REJECTED`, `WITHDRAWN` | +| `vote_outcome` | `ADOPTED`, `REJECTED`, `SENT_BACK` | + +### Modified Existing Tables + +**`committee`** — Add fields: +| New Column | Type | Notes | +|------------|------|-------| +| `max_draft_resolutions` | `smallint` default 3 | Configurable per committee (GO default: 3) | +| `active_draft_resolution_id` | `text` FK→resolutionPaper, nullable | Which DR is currently being debated | +| `current_operative_index` | `smallint` nullable | Which operative paragraph is active (0-indexed). Amendments locked for index < this | +| `support_re_evaluation_open` | `boolean` default false | Whether delegations can currently change DR support | + +### New Tables (11) + +**`resolution_paper`** — Single entity for the entire lifecycle +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `committee_id` | `text` FK→committee | | +| `agenda_item_id` | `text` FK→agendaItem | | +| `creator_committee_member_id` | `text` FK→committeeMember | Only delegations can create | +| `status` | `paper_status` | Default `WORKING_PAPER` | +| `content` | `jsonb` | Resolution JSON (validated with `ResolutionSchema`) | +| `title` | `text` | Optional working title | +| `document_number` | `text` | Set when promoted (e.g. `"I/DR.1"`) | +| `sequence_number` | `smallint` | Sequential DR number within agenda item | +| `created_at`, `updated_at` | `timestamp` | | + +**`paper_content_snapshot`** — Version history at key transitions +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `content` | `jsonb` | Resolution JSON at snapshot time | +| `trigger` | `text` | What triggered the snapshot (e.g. `"promoted_to_dr"`, `"amendment_accepted"`) | +| `created_at` | `timestamp` | | + +**`paper_sponsor`** — Delegation sponsors (one unified role) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `committee_member_id` | `text` FK→committeeMember | Delegations only | +| `created_at`, `updated_at` | `timestamp` | | +| | | UNIQUE(paper_id, committee_member_id) | + +**`paper_share_code`** — Owner-created invitation codes +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `code` | `text` UNIQUE | 6-char alphanumeric | +| `permission` | `share_code_permission` | `SPONSOR` or `EDIT` | +| `created_at`, `updated_at` | `timestamp` | | + +**`paper_editor`** — Edit access (supports NSAs via conferenceUser) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `conference_user_id` | `text` FK→conferenceUser | Works for delegates AND NSAs | +| `created_at`, `updated_at` | `timestamp` | | +| | | UNIQUE(paper_id, conference_user_id) | + +**`paper_clause_lock`** — Pessimistic per-clause editing locks (collaborative mode) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `clause_id` | `text` | Clause `id` from JSON | +| `conference_user_id` | `text` FK→conferenceUser | Lock holder | +| `expires_at` | `timestamp` | TTL (60s from acquire/refresh) | +| `created_at`, `updated_at` | `timestamp` | | +| | | UNIQUE(paper_id, clause_id) | + +**`resolution_comment`** — Comments on draft resolutions (paragraph + document level) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `clause_id` | `text` nullable | `null` = document-level; otherwise clause `id` from JSON | +| `author_conference_user_id` | `text` FK→conferenceUser | | +| `content` | `text` | | +| `visibility` | `comment_visibility` | Default `PUBLIC` | +| `parent_comment_id` | `text` FK→self, nullable | Threading | +| `created_at`, `updated_at` | `timestamp` | | + +**`amendment`** — Formal amendments to operative clauses (§17) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `proposer_committee_member_id` | `text` FK→committeeMember | | +| `type` | `amendment_type` | DELETE, ADD, ALTER_TEXT, ALTER_POSITION | +| `status` | `amendment_status` | Default `PENDING` | +| `target_clause_id` | `text` | Operative clause `id` from JSON (for DELETE/ALTER_TEXT/ALTER_POSITION) | +| `target_operative_index` | `smallint` | Index of targeted paragraph (for submission locking) | +| `new_content` | `jsonb` nullable | OperativeClause JSON for ADD/ALTER_TEXT | +| `target_position` | `smallint` nullable | For ADD/ALTER_POSITION | +| `created_at`, `updated_at` | `timestamp` | | + +**`amendment_sponsor`** — Sponsors supporting an amendment +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `amendment_id` | `text` FK→amendment | | +| `committee_member_id` | `text` FK→committeeMember | | +| `created_at`, `updated_at` | `timestamp` | | +| | | UNIQUE(amendment_id, committee_member_id) | + +**`operative_clause_vote`** — Per-paragraph vote results (§12.5) +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper | | +| `clause_id` | `text` | Operative clause `id` from JSON | +| `outcome` | `vote_outcome` | ADOPTED or REJECTED | +| `votes_for` | `smallint` | | +| `votes_against` | `smallint` | | +| `votes_abstain` | `smallint` default 0 | | +| `created_at` | `timestamp` | | + +**`resolution_vote_result`** — Final vote outcome on the DR as a whole +| Column | Type | Notes | +|--------|------|-------| +| `id` | `text` (nanoid) | PK | +| `paper_id` | `text` FK→resolutionPaper, UNIQUE | | +| `outcome` | `vote_outcome` | `ADOPTED` or `REJECTED` | +| `votes_for` | `smallint` | | +| `votes_against` | `smallint` | | +| `votes_abstain` | `smallint` default 0 | | +| `created_at`, `updated_at` | `timestamp` | | + +### Key Relations + +- `resolutionPaper` → committee, agendaItem, creator, sponsors[], shareCodes[], editors[], comments[], amendments[], clauseVotes[], voteResult?, snapshots[] +- `committee` → resolutionPapers[], activeDraftResolution? +- `amendment` → paper, proposer, sponsors[] +- `resolutionComment` → paper, author, parentComment?, replies[] + +--- + +## New Pages & Routes + +### Participant (Delegates / NSAs) + +| Route | Purpose | +| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.../participant/[committeeId]/papers/+page.svelte` | Papers overview: "My Papers", "Enter Code" input, create button, published DRs list, amendment submission (when amendment phase active) | +| `.../participant/[committeeId]/papers/[paperId]/+page.svelte` | Paper detail: editor (if access), sponsor list, share codes (creator), submit button. For DRs: read-only preview, comments, support toggle (during re-evaluation) | + +### Chair + +| Route | Purpose | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `.../(chairs)/resolutions/+page.svelte` | **New 5th tab (alt+5)**: Submitted papers queue, DR management, per-paragraph debate progression, amendment processing with consensus check, paragraph + final vote recording | + +Modify: `ChairNavbar.svelte` — add 5th `fa-scroll` button with `alt+5`. + +### Mission Control + +| Route | Purpose | +| ---------------------------------------------- | --------------------------------------------------------------- | +| `.../mission-control/resolutions/+page.svelte` | Cross-committee resolution overview, status filters, commenting | + +### Presentation + +Modify presentation view — add optional `ResolutionPreview` grid item, pushed by chair via `localDB.committeeSettings.presentationDraftResolutionId`. + +--- + +## Resolution Editor Library Changes + +**Location**: `../munify-resolution-editor/src/lib/` + +### 1. Preamble Extension Points (currently missing) + +```typescript +preambleClauseToolbar?: Snippet<[{ clause: PreambleClause; index: number }]>; +preambleClauseAnnotations?: Snippet<[{ clause: PreambleClause; index: number }]>; +``` + +### 2. Amendment Rendering Props + +```typescript +amendments?: AmendmentOverlay[]; +rejectedClauseIds?: string[]; // For §12.5 paragraph vote results +onAmendmentClick?: (amendmentId: string) => void; +``` + +Renders: strikethrough (DELETE), green insertion (ADD), diff view (ALTER_TEXT), arrow (ALTER_POSITION). Rejected clauses shown with strikethrough/dimmed. + +### 3. Between-Clauses Snippet + +```typescript +betweenOperativeClauses?: Snippet<[{ index: number }]>; +``` + +For ADD amendment insertion markers. + +### 4. Comment Panel Integration Point + +```typescript +commentPanel?: Snippet<[{ resolution: Resolution; activeClauseId?: string }]>; +``` + +--- + +## GraphQL Handlers + +### New Handler Files (10) + +| Handler | Key Mutations | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| `resolutionPaper.ts` | `createResolutionPaper`, `updatePaperContent`, `submitPaper`, `promoteToDraftResolution`, `recordVoteResult` | +| `paperSponsor.ts` | `addSponsor`, `removeSponsor` | +| `paperShareCode.ts` | `createShareCode`, `deleteShareCode`, `redeemShareCode` | +| `paperEditor.ts` | managed via share code redemption | +| `paperContentSnapshot.ts` | auto-created on status transitions | +| `resolutionComment.ts` | `createComment`, `deleteComment` | +| `amendment.ts` | `createAmendment`, `submitAmendment`, `adoptByConsensus`, `acceptAmendment`, `rejectAmendment`, `withdrawAmendment` | +| `amendmentSponsor.ts` | `addAmendmentSponsor`, `removeAmendmentSponsor` | +| `operativeClauseVote.ts` | `recordClauseVote` (per-paragraph) | +| `resolutionVoteResult.ts` | via `recordVoteResult` on paper handler | + +### Key Mutations on `committee` handler (extend existing) + +- `setActiveDraftResolution(committeeId, paperId)` — sets which DR is being debated +- `setCurrentOperativeIndex(committeeId, index)` — advances paragraph pointer (locks amendments for passed paragraphs) +- `toggleSupportReEvaluation(committeeId, open)` — opens/closes support re-evaluation phase + +### Amendment Flow (GO §17 compliant) + +1. **Creation**: Delegate creates amendment → status `PENDING`. System validates `targetOperativeIndex >= committee.currentOperativeIndex` (can't amend already-passed paragraphs, §17.3) +2. **Sponsoring**: Other delegations sponsor. System enforces `sponsorCount >= committee.paperSupportThreshold` (10% of present, §17.1) +3. **Submission**: Once threshold met, proposer submits → status `SUBMITTED` +4. **Chair processes** (in GO-prescribed order): + - Amendments targeting current paragraph first + - Among those, most far-reaching first (§17.3, chair determines order) + - Chair presents amendment, then clicks **"Check Consensus"** + - **No objection** → `adoptByConsensus` → status `CONSENSUS_ADOPTED`, amendment applied to JSON + - **Objection** → debate → formal vote → `acceptAmendment`/`rejectAmendment` +5. **Withdrawal**: Proposer can withdraw (§17.5). Chair asks if another delegation wants to maintain it. + +### Share Code Flow + +1. Enter code → `redeemShareCode` returns paper ID + permission (no records created) +2. SPONSOR: navigate to paper → review → click "Sponsor" → `addSponsor` +3. EDIT: creates `paperEditor` record immediately → navigate to paper in edit mode +4. NSAs: can only redeem EDIT codes + +### Authorization Summary + +| Action | Who | +| --------------------------------- | --------------------------------------------------------------- | +| Create working paper | DELEGATE only | +| Edit working paper | Creator + editors (via share code) | +| Edit submitted paper | Creator + editors + Sekretariat (TEAM) + Chair | +| Sponsor paper | Any DELEGATE (via sponsor code); multi-support allowed for DRs | +| Submit working paper | Creator | +| View draft resolution | All conference users | +| Edit draft resolution | Chair + Sekretariat (TEAM) | +| Comment on DR | All conference users (TEAM_ONLY visibility for TEAM/ADMIN only) | +| Promote WP → DR | Chair / Admin | +| Open/close support re-evaluation | Chair | +| Set active DR / advance paragraph | Chair | +| Create/sponsor amendment | Any DELEGATE | +| Submit amendment | Proposer (when threshold met) | +| Consensus check / accept / reject | Chair | +| Record paragraph vote | Chair | +| Record final vote | Chair | + +--- + +## Implementation Phases + +### Phase 1: Database Schema + Basic API ✅ + +- ~~Add editor library dependency~~ +- ~~Add 6 enums, 10 new tables, 4 new committee columns to `schema.ts`~~ +- ~~Add all relations to `relations.ts`~~ +- ~~`bun run db:push`~~ +- ~~Create handlers: `resolutionPaper`, `paperSponsor`, `paperShareCode`, `paperEditor`, `paperContentSnapshot`~~ +- ~~Register handlers, add i18n messages~~ + +### Phase 2: Delegate Working Paper UI ✅ + +- ~~Papers overview page + paper detail/editor page~~ +- ~~`ResolutionEditor` integration with `onResolutionChange` (debounced 500ms)~~ +- ~~Share code creation, copying, redemption~~ +- ~~Sponsor list + "Sponsor" flow~~ +- ~~"Submit to Chair" button~~ +- ~~Navigation from participant committee page~~ +- ~~Clause-level locking: `paper_clause_lock` table, acquire/release/heartbeat mutations, subscription, lock-aware content merge~~ +- ~~Click-to-lock UX: hover overlay ("Start editing"), inline "Done editing" button, `collaborativeMode` gate~~ + +### Phase 3: Chair Resolutions Tab + DR Promotion ✅ + +- ~~New 5th tab in `ChairNavbar` (alt+5, `fa-scroll`) — converted to bottom dock~~ +- ~~Submitted papers queue (ranked by sponsor count, suggests top N)~~ +- ~~"Promote to Draft Resolution" with auto-numbering~~ +- ~~DR editor for chair + Sekretariat (parallel access, clause locking)~~ +- ~~Content snapshot on promotion~~ +- ~~Title hidden post-promotion (document number is sole identifier for DRs)~~ + +### Phase 4: Support Re-evaluation + DR Ordering ✅ + +- ~~"Open/Close Re-evaluation" chair controls~~ +- ~~Delegate UI: add/remove support on DRs (multi-support)~~ +- ~~Dynamic sponsor count display + DR ranking~~ +- ~~"Set Active DR" for debate progression~~ +- ~~Chair sponsor management: add/remove any member as sponsor (bypasses all gates), searchable modal, sorted display~~ + +### Phase 5: Comment System ✅ + +- ~~`resolutionComment` handler (create, update, delete, threading, PUBLIC/TEAM_ONLY visibility)~~ +- ~~`CommentSection.svelte` (document + clause level, threading, visibility)~~ +- ~~**Editor library**: preamble extension points (preambleAnnotations, preambleClauseToolbar)~~ +- ~~Integration in chair + participant DR views with real-time subscriptions~~ + +### Phase 6: Amendment System (in progress) + +**6a: Editor library** ✅ — amendment overlay props, `rejectedClauseIds`, between-clauses snippet, rendering +**6b: Backend** ✅ — amendment + sponsor handlers, threshold enforcement, consensus check flow, paragraph index locking, amendment application to JSON + snapshot +**6c: Chair UI** ✅ (WIP) — per-paragraph debate controls, amendment queue (GO-ordered), consensus check button, accept/reject actions, start amendment phase button +**6d: Delegate UI** — amendment creation form (4 types), sponsor flow + +### Phase 7: Voting (Paragraphs + Final) + +- `operativeClauseVote` handler — per-paragraph vote recording, rejected paragraphs hidden in preview +- `resolutionVoteResult` handler — final roll-call vote (absolute majority) +- Set `committee.lastResolutionAdoptionDate` on ADOPTED (triggers existing confetti) +- DR rejection fallback: system highlights next DR by supporter count +- Final resolution display with vote counts + +### Phase 8: Presentation + Mission Control + +- `presentationDraftResolutionId` in Dexie localDB +- "Push to Presentation" button +- `ResolutionPreview` grid item in presentation view +- Mission Control resolutions overview page + +--- + +## Key Architectural Decisions + +**Storage**: Resolution content as single JSONB column. Amendments + paragraph votes reference clause IDs from the JSON. Snapshots preserve history at key transitions. + +**Real-time**: Rumble pubsub → Houdini subscriptions. `onResolutionChange` (debounced 500ms) → mutation → pubsub. Last-writer-wins acceptable for MUN context. + +**Clause-level locking**: Explicit click-to-lock UX (not focus/blur). Delegates hover an unlocked clause to see a "Start editing" overlay, click to acquire a server-side lock, and click "Done editing" to release. Locks are per-clause rows in `paper_clause_lock` with a 60s TTL. A hybrid heartbeat (30s interval, only fires when idle >25s) keeps locks alive during active editing — saves already refresh locks implicitly via `updatePaperContent`. Lock state is pushed via GraphQL subscription; optimistic IDs bridge the gap. `collaborativeMode` gates all lock UI: solo editing (no share codes used, working paper status) shows no overlays or lock buttons. The `beforeunload` handler and navigation cleanup release all held locks via `sendBeacon`. + +**Amendment application**: When accepted (by consensus or vote), server mutates JSON + creates snapshot. DELETE removes clause, ADD inserts, ALTER_TEXT replaces blocks, ALTER_POSITION moves. Record keeps status for history. + +**Paragraph debate tracking**: Committee-level `currentOperativeIndex` field. Server-side enforcement: `createAmendment` rejects if `targetOperativeIndex < currentOperativeIndex`. Chair advances via `setCurrentOperativeIndex`. + +**Paragraph vote-downs**: Rejected clauses stay in JSON but their `id`s are tracked in `operativeClauseVote`. Editor library renders them with strikethrough/hidden via `rejectedClauseIds` prop. + +**Document numbering**: On promotion, auto-increment `sequenceNumber` per committee+agendaItem. `fullDocumentNumber` computed as `committee.abbreviation + "/" + toRoman(agendaItemPosition) + "/DR." + sequenceNumber`. + +**Validation**: `ResolutionSchema` (Zod) validates JSONB on every save. + +**Support re-evaluation**: Committee-level `supportReEvaluationOpen` boolean. When open, delegates can add/remove `paperSponsor` on DRs. Multi-support allowed (no exclusivity). + +--- + +## Verification Plan + +Per phase: + +1. `bun run db:push` succeeds +2. `bun run check` + `bun run lint` pass +3. Manual flow: create paper → share → sponsor → submit → Sekretariat edits → chair promotes → re-evaluation → debate DR → per-paragraph amendments (consensus + vote) → paragraph votes → final vote → adopted/rejected +4. Real-time: two browser tabs, verify subscription updates +5. Authorization: NSAs can't create/sponsor, delegates can't edit DRs, amendments locked for passed paragraphs +6. Edge cases: DR rejection fallback, amendment withdrawal, paragraph vote-down removal + +--- + +## Critical Files + +| File | Changes | +| -------------------------------------------------------- | ------------------------------------------------------------- | +| `chase/src/api/db/schema.ts` | 6 enums, 10 tables, 4 committee columns | +| `chase/src/api/db/relations.ts` | All new + reverse relations | +| `chase/src/api/handlers/register.ts` | Import 10 new handler files | +| `chase/src/api/handlers/committee.ts` | New mutations for DR tracking, re-evaluation, paragraph index | +| `chase/src/routes/.../ChairNavbar.svelte` | 5th tab | +| `chase/src/routes/.../participant/[committeeId]/papers/` | New routes | +| `chase/src/routes/.../(chairs)/resolutions/` | New route | +| `chase/src/routes/.../mission-control/resolutions/` | New route | +| `munify-resolution-editor/.../ResolutionEditor.svelte` | Preamble snippets, amendment + rejected clause props | +| `munify-resolution-editor/.../ResolutionPreview.svelte` | Amendment rendering, comment panel | +| `munify-resolution-editor/.../schema/resolution.ts` | AmendmentOverlay type | +| `chase/messages/en.json` + `de.json` | All new i18n strings | diff --git a/drizzle/0005_tan_paibok.sql b/drizzle/0005_tan_paibok.sql new file mode 100644 index 00000000..172f87c9 --- /dev/null +++ b/drizzle/0005_tan_paibok.sql @@ -0,0 +1,159 @@ +CREATE TYPE "public"."amendment_status" AS ENUM('PENDING', 'SUBMITTED', 'CONSENSUS_ADOPTED', 'ACCEPTED', 'REJECTED', 'WITHDRAWN');--> statement-breakpoint +CREATE TYPE "public"."amendment_type" AS ENUM('DELETE', 'ADD', 'ALTER_TEXT', 'ALTER_POSITION');--> statement-breakpoint +CREATE TYPE "public"."comment_visibility" AS ENUM('PUBLIC', 'TEAM_ONLY');--> statement-breakpoint +CREATE TYPE "public"."paper_status" AS ENUM('WORKING_PAPER', 'SUBMITTED', 'DRAFT_RESOLUTION', 'AMENDMENT_PHASE', 'VOTING_PHASE', 'FINAL');--> statement-breakpoint +CREATE TYPE "public"."share_code_permission" AS ENUM('SPONSOR', 'EDIT');--> statement-breakpoint +CREATE TYPE "public"."vote_outcome" AS ENUM('ADOPTED', 'REJECTED', 'SENT_BACK');--> statement-breakpoint +CREATE TABLE "amendment" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "proposer_committee_member_id" text NOT NULL, + "type" "amendment_type" NOT NULL, + "status" "amendment_status" DEFAULT 'PENDING' NOT NULL, + "target_clause_id" text, + "target_operative_index" smallint, + "new_content" json, + "target_position" smallint, + "document_number" text, + "sequence_number" smallint +); +--> statement-breakpoint +CREATE TABLE "amendment_sponsor" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "amendment_id" text NOT NULL, + "committee_member_id" text NOT NULL, + CONSTRAINT "amendment_sponsor_amendmentId_committeeMemberId_unique" UNIQUE("amendment_id","committee_member_id") +); +--> statement-breakpoint +CREATE TABLE "operative_clause_vote" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "clause_id" text NOT NULL, + "outcome" "vote_outcome" NOT NULL, + "votes_for" integer NOT NULL, + "votes_against" integer NOT NULL, + "votes_abstain" integer DEFAULT 0 NOT NULL, + CONSTRAINT "operative_clause_vote_paperId_clauseId_unique" UNIQUE("paper_id","clause_id") +); +--> statement-breakpoint +CREATE TABLE "paper_clause_lock" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "clause_id" text NOT NULL, + "conference_user_id" text NOT NULL, + "acquired_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "paper_clause_lock_paperId_clauseId_unique" UNIQUE("paper_id","clause_id") +); +--> statement-breakpoint +CREATE TABLE "paper_content_snapshot" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "content" json, + "trigger" text +); +--> statement-breakpoint +CREATE TABLE "paper_editor" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "conference_user_id" text NOT NULL, + CONSTRAINT "paper_editor_paperId_conferenceUserId_unique" UNIQUE("paper_id","conference_user_id") +); +--> statement-breakpoint +CREATE TABLE "paper_share_code" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "code" text NOT NULL, + "permission" "share_code_permission" NOT NULL, + CONSTRAINT "paper_share_code_code_unique" UNIQUE("code") +); +--> statement-breakpoint +CREATE TABLE "paper_sponsor" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "committee_member_id" text NOT NULL, + CONSTRAINT "paper_sponsor_paperId_committeeMemberId_unique" UNIQUE("paper_id","committee_member_id") +); +--> statement-breakpoint +CREATE TABLE "resolution_comment" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "clause_id" text, + "author_conference_user_id" text NOT NULL, + "content" text NOT NULL, + "visibility" "comment_visibility" DEFAULT 'PUBLIC' NOT NULL, + "parent_comment_id" text +); +--> statement-breakpoint +CREATE TABLE "resolution_paper" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "committee_id" text NOT NULL, + "agenda_item_id" text NOT NULL, + "creator_committee_member_id" text NOT NULL, + "status" "paper_status" DEFAULT 'WORKING_PAPER' NOT NULL, + "content" json, + "title" text, + "document_number" text, + "sequence_number" smallint, + "deleted_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "resolution_vote_result" ( + "id" text PRIMARY KEY NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now(), + "paper_id" text NOT NULL, + "outcome" "vote_outcome" NOT NULL, + "votes_for" integer NOT NULL, + "votes_against" integer NOT NULL, + "votes_abstain" integer DEFAULT 0 NOT NULL, + CONSTRAINT "resolution_vote_result_paperId_unique" UNIQUE("paper_id") +); +--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "max_draft_resolutions" smallint DEFAULT 3 NOT NULL;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "active_draft_resolution_id" text;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "current_operative_index" smallint;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "support_re_evaluation_open" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "active_amendment_id" text;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "resolution_headline" text;--> statement-breakpoint +ALTER TABLE "amendment" ADD CONSTRAINT "amendment_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "amendment" ADD CONSTRAINT "amendment_proposer_committee_member_id_committee_member_id_fk" FOREIGN KEY ("proposer_committee_member_id") REFERENCES "public"."committee_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "amendment_sponsor" ADD CONSTRAINT "amendment_sponsor_amendment_id_amendment_id_fk" FOREIGN KEY ("amendment_id") REFERENCES "public"."amendment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "amendment_sponsor" ADD CONSTRAINT "amendment_sponsor_committee_member_id_committee_member_id_fk" FOREIGN KEY ("committee_member_id") REFERENCES "public"."committee_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "operative_clause_vote" ADD CONSTRAINT "operative_clause_vote_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_clause_lock" ADD CONSTRAINT "paper_clause_lock_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_clause_lock" ADD CONSTRAINT "paper_clause_lock_conference_user_id_conference_user_id_fk" FOREIGN KEY ("conference_user_id") REFERENCES "public"."conference_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_content_snapshot" ADD CONSTRAINT "paper_content_snapshot_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_editor" ADD CONSTRAINT "paper_editor_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_editor" ADD CONSTRAINT "paper_editor_conference_user_id_conference_user_id_fk" FOREIGN KEY ("conference_user_id") REFERENCES "public"."conference_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_share_code" ADD CONSTRAINT "paper_share_code_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_sponsor" ADD CONSTRAINT "paper_sponsor_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "paper_sponsor" ADD CONSTRAINT "paper_sponsor_committee_member_id_committee_member_id_fk" FOREIGN KEY ("committee_member_id") REFERENCES "public"."committee_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_comment" ADD CONSTRAINT "resolution_comment_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_comment" ADD CONSTRAINT "resolution_comment_author_conference_user_id_conference_user_id_fk" FOREIGN KEY ("author_conference_user_id") REFERENCES "public"."conference_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_comment" ADD CONSTRAINT "resolution_comment_parent_comment_id_resolution_comment_id_fk" FOREIGN KEY ("parent_comment_id") REFERENCES "public"."resolution_comment"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_paper" ADD CONSTRAINT "resolution_paper_committee_id_committee_id_fk" FOREIGN KEY ("committee_id") REFERENCES "public"."committee"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_paper" ADD CONSTRAINT "resolution_paper_agenda_item_id_agenda_item_id_fk" FOREIGN KEY ("agenda_item_id") REFERENCES "public"."agenda_item"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_paper" ADD CONSTRAINT "resolution_paper_creator_committee_member_id_committee_member_id_fk" FOREIGN KEY ("creator_committee_member_id") REFERENCES "public"."committee_member"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resolution_vote_result" ADD CONSTRAINT "resolution_vote_result_paper_id_resolution_paper_id_fk" FOREIGN KEY ("paper_id") REFERENCES "public"."resolution_paper"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "committee" ADD CONSTRAINT "committee_active_draft_resolution_id_resolution_paper_id_fk" FOREIGN KEY ("active_draft_resolution_id") REFERENCES "public"."resolution_paper"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "committee" ADD CONSTRAINT "committee_active_amendment_id_amendment_id_fk" FOREIGN KEY ("active_amendment_id") REFERENCES "public"."amendment"("id") ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/0006_cuddly_sersi.sql b/drizzle/0006_cuddly_sersi.sql new file mode 100644 index 00000000..c241f849 --- /dev/null +++ b/drizzle/0006_cuddly_sersi.sql @@ -0,0 +1,2 @@ +ALTER TABLE "committee" ADD COLUMN "amendment_submission_open" boolean DEFAULT true NOT NULL;--> statement-breakpoint +ALTER TABLE "committee" ADD COLUMN "amendment_sponsoring_open" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/0007_typical_the_renegades.sql b/drizzle/0007_typical_the_renegades.sql new file mode 100644 index 00000000..13ec3697 --- /dev/null +++ b/drizzle/0007_typical_the_renegades.sql @@ -0,0 +1,2 @@ +ALTER TABLE "committee" ADD COLUMN "current_operative_clause_id" text;--> statement-breakpoint +ALTER TABLE "conference" ADD COLUMN "resolution_feature_enabled" boolean DEFAULT true NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 00000000..aad4cbb5 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,2409 @@ +{ + "id": "f08afff5-d54b-4577-be17-3d085825de7d", + "prevId": "b00cbd69-00a6-4d01-8161-c94cbd1fb704", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agenda_item": { + "name": "agenda_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agenda_item_committee_id_committee_id_fk": { + "name": "agenda_item_committee_id_committee_id_fk", + "tableFrom": "agenda_item", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment": { + "name": "amendment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposer_committee_member_id": { + "name": "proposer_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "amendment_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "amendment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "target_clause_id": { + "name": "target_clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_operative_index": { + "name": "target_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "new_content": { + "name": "new_content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "target_position": { + "name": "target_position", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_paper_id_resolution_paper_id_fk": { + "name": "amendment_paper_id_resolution_paper_id_fk", + "tableFrom": "amendment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_proposer_committee_member_id_committee_member_id_fk": { + "name": "amendment_proposer_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment", + "tableTo": "committee_member", + "columnsFrom": [ + "proposer_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment_sponsor": { + "name": "amendment_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "amendment_id": { + "name": "amendment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_sponsor_amendment_id_amendment_id_fk": { + "name": "amendment_sponsor_amendment_id_amendment_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "amendment", + "columnsFrom": [ + "amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_sponsor_committee_member_id_committee_member_id_fk": { + "name": "amendment_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "amendment_sponsor_amendmentId_committeeMemberId_unique": { + "name": "amendment_sponsor_amendmentId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "amendment_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee": { + "name": "committee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whiteboard_content": { + "name": "whiteboard_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'

'" + }, + "show_whiteboard": { + "name": "show_whiteboard", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "committee_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SUSPENSION'" + }, + "status_headline": { + "name": "status_headline", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status_until": { + "name": "status_until", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "state_of_debate": { + "name": "state_of_debate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_delegations_to_add_themselves_to_speakers_list": { + "name": "allow_delegations_to_add_themselves_to_speakers_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_agenda_item_id": { + "name": "active_agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_simple_majority": { + "name": "custom_simple_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_two_thirds_majority": { + "name": "custom_two_thirds_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_paper_support_threshold": { + "name": "custom_paper_support_threshold", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "last_resolution_adoption_date": { + "name": "last_resolution_adoption_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_draft_resolutions": { + "name": "max_draft_resolutions", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "active_draft_resolution_id": { + "name": "active_draft_resolution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_operative_index": { + "name": "current_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "support_re_evaluation_open": { + "name": "support_re_evaluation_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_amendment_id": { + "name": "active_amendment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_headline": { + "name": "resolution_headline", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "committee_conference_id_conference_id_fk": { + "name": "committee_conference_id_conference_id_fk", + "tableFrom": "committee", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_active_agenda_item_id_agenda_item_id_fk": { + "name": "committee_active_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee", + "tableTo": "agenda_item", + "columnsFrom": [ + "active_agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_draft_resolution_id_resolution_paper_id_fk": { + "name": "committee_active_draft_resolution_id_resolution_paper_id_fk", + "tableFrom": "committee", + "tableTo": "resolution_paper", + "columnsFrom": [ + "active_draft_resolution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_amendment_id_amendment_id_fk": { + "name": "committee_active_amendment_id_amendment_id_fk", + "tableFrom": "committee", + "tableTo": "amendment", + "columnsFrom": [ + "active_amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "committee_conferenceId_name_unique": { + "name": "committee_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "committee_conferenceId_abbreviation_unique": { + "name": "committee_conferenceId_abbreviation_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "abbreviation" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_member": { + "name": "committee_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "present": { + "name": "present", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_member_committee_id_committee_id_fk": { + "name": "committee_member_committee_id_committee_id_fk", + "tableFrom": "committee_member", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_member_representation_id_representation_id_fk": { + "name": "committee_member_representation_id_representation_id_fk", + "tableFrom": "committee_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_topic_changed_timestamp": { + "name": "committee_topic_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_topic_changed_timestamp_committee_id_committee_id_fk": { + "name": "committee_topic_changed_timestamp_committee_id_committee_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk": { + "name": "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference": { + "name": "conference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "press_website": { + "name": "press_website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_moderated_caucus": { + "name": "has_moderated_caucus", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_member": { + "name": "conference_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "conference_member_conference_id_conference_id_fk": { + "name": "conference_member_conference_id_conference_id_fk", + "tableFrom": "conference_member", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_member_representation_id_representation_id_fk": { + "name": "conference_member_representation_id_representation_id_fk", + "tableFrom": "conference_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_user": { + "name": "conference_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_user_type": { + "name": "conference_user_type", + "type": "conference_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "conference_user_conference_id_conference_id_fk": { + "name": "conference_user_conference_id_conference_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_conference_member_id_conference_member_id_fk": { + "name": "conference_user_conference_member_id_conference_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_committee_member_id_committee_member_id_fk": { + "name": "conference_user_committee_member_id_committee_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operative_clause_vote": { + "name": "operative_clause_vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "operative_clause_vote_paper_id_resolution_paper_id_fk": { + "name": "operative_clause_vote_paper_id_resolution_paper_id_fk", + "tableFrom": "operative_clause_vote", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "operative_clause_vote_paperId_clauseId_unique": { + "name": "operative_clause_vote_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_clause_lock": { + "name": "paper_clause_lock", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "paper_clause_lock_paper_id_resolution_paper_id_fk": { + "name": "paper_clause_lock_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_clause_lock_conference_user_id_conference_user_id_fk": { + "name": "paper_clause_lock_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_clause_lock_paperId_clauseId_unique": { + "name": "paper_clause_lock_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_content_snapshot": { + "name": "paper_content_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "paper_content_snapshot_paper_id_resolution_paper_id_fk": { + "name": "paper_content_snapshot_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_content_snapshot", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_editor": { + "name": "paper_editor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_editor_paper_id_resolution_paper_id_fk": { + "name": "paper_editor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_editor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_editor_conference_user_id_conference_user_id_fk": { + "name": "paper_editor_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_editor", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_editor_paperId_conferenceUserId_unique": { + "name": "paper_editor_paperId_conferenceUserId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "conference_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_share_code": { + "name": "paper_share_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "share_code_permission", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_share_code_paper_id_resolution_paper_id_fk": { + "name": "paper_share_code_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_share_code", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_share_code_code_unique": { + "name": "paper_share_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_sponsor": { + "name": "paper_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_sponsor_paper_id_resolution_paper_id_fk": { + "name": "paper_sponsor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_sponsor_committee_member_id_committee_member_id_fk": { + "name": "paper_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_sponsor_paperId_committeeMemberId_unique": { + "name": "paper_sponsor_paperId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.presence_changed_timestamp": { + "name": "presence_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "present_set_to": { + "name": "present_set_to", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "presence_changed_timestamp_committee_member_id_committee_member_id_fk": { + "name": "presence_changed_timestamp_committee_member_id_committee_member_id_fk", + "tableFrom": "presence_changed_timestamp", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.representation": { + "name": "representation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha2_code": { + "name": "alpha2_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha3_code": { + "name": "alpha3_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "representation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fa_icon": { + "name": "fa_icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regional_group": { + "name": "regional_group", + "type": "regional_group", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "representation_conference_id_conference_id_fk": { + "name": "representation_conference_id_conference_id_fk", + "tableFrom": "representation", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "representation_conferenceId_name_unique": { + "name": "representation_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "representation_conferenceId_alpha2Code_alpha3Code_unique": { + "name": "representation_conferenceId_alpha2Code_alpha3Code_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "alpha2_code", + "alpha3_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_comment": { + "name": "resolution_comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_conference_user_id": { + "name": "author_conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "comment_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLIC'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_comment_paper_id_resolution_paper_id_fk": { + "name": "resolution_comment_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_author_conference_user_id_conference_user_id_fk": { + "name": "resolution_comment_author_conference_user_id_conference_user_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "conference_user", + "columnsFrom": [ + "author_conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_parent_comment_id_resolution_comment_id_fk": { + "name": "resolution_comment_parent_comment_id_resolution_comment_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_comment", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_paper": { + "name": "resolution_paper", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_committee_member_id": { + "name": "creator_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "paper_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'WORKING_PAPER'" + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_paper_committee_id_committee_id_fk": { + "name": "resolution_paper_committee_id_committee_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_agenda_item_id_agenda_item_id_fk": { + "name": "resolution_paper_agenda_item_id_agenda_item_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_creator_committee_member_id_committee_member_id_fk": { + "name": "resolution_paper_creator_committee_member_id_committee_member_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee_member", + "columnsFrom": [ + "creator_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_vote_result": { + "name": "resolution_vote_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_vote_result_paper_id_resolution_paper_id_fk": { + "name": "resolution_vote_result_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_vote_result", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resolution_vote_result_paperId_unique": { + "name": "resolution_vote_result_paperId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speaker_on_list": { + "name": "speaker_on_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "overwrite_name": { + "name": "overwrite_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "speaker_on_list_committee_member_id_committee_member_id_fk": { + "name": "speaker_on_list_committee_member_id_committee_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_conference_member_id_conference_member_id_fk": { + "name": "speaker_on_list_conference_member_id_conference_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_speakers_list_id_speakers_list_id_fk": { + "name": "speaker_on_list_speakers_list_id_speakers_list_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speaker_on_list_speakersListId_position_unique": { + "name": "speaker_on_list_speakersListId_position_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "position" + ] + }, + "speaker_on_list_speakersListId_committeeMemberId_unique": { + "name": "speaker_on_list_speakersListId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "committee_member_id" + ] + }, + "speaker_on_list_speakersListId_conferenceMemberId_unique": { + "name": "speaker_on_list_speakersListId_conferenceMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "conference_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speakers_list": { + "name": "speakers_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "speakers_list_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "speaking_time": { + "name": "speaking_time", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "time_left": { + "name": "time_left", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_closed": { + "name": "is_closed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "speakers_list_agenda_item_id_agenda_item_id_fk": { + "name": "speakers_list_agenda_item_id_agenda_item_id_fk", + "tableFrom": "speakers_list", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speakers_list_agendaItemId_type_unique": { + "name": "speakers_list_agendaItemId_type_unique", + "nullsNotDistinct": false, + "columns": [ + "agenda_item_id", + "type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spoken_time_period": { + "name": "spoken_time_period", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_timestamp": { + "name": "end_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "spoken_time_period_committee_member_id_committee_member_id_fk": { + "name": "spoken_time_period_committee_member_id_committee_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_conference_member_id_conference_member_id_fk": { + "name": "spoken_time_period_conference_member_id_conference_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_speakers_list_id_speakers_list_id_fk": { + "name": "spoken_time_period_speakers_list_id_speakers_list_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_username": { + "name": "preferred_username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_id_unique": { + "name": "user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + }, + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.amendment_status": { + "name": "amendment_status", + "schema": "public", + "values": [ + "PENDING", + "SUBMITTED", + "CONSENSUS_ADOPTED", + "ACCEPTED", + "REJECTED", + "WITHDRAWN" + ] + }, + "public.amendment_type": { + "name": "amendment_type", + "schema": "public", + "values": [ + "DELETE", + "ADD", + "ALTER_TEXT", + "ALTER_POSITION" + ] + }, + "public.comment_visibility": { + "name": "comment_visibility", + "schema": "public", + "values": [ + "PUBLIC", + "TEAM_ONLY" + ] + }, + "public.committee_status": { + "name": "committee_status", + "schema": "public", + "values": [ + "FORMAL", + "INFORMAL", + "MODERATED_INFORMAL", + "PAUSE", + "SUSPENSION" + ] + }, + "public.conference_user_type": { + "name": "conference_user_type", + "schema": "public", + "values": [ + "ADMIN", + "TEAM", + "SPECTATOR", + "DELEGATE", + "NON_STATE_ACTOR" + ] + }, + "public.paper_status": { + "name": "paper_status", + "schema": "public", + "values": [ + "WORKING_PAPER", + "SUBMITTED", + "DRAFT_RESOLUTION", + "AMENDMENT_PHASE", + "VOTING_PHASE", + "FINAL" + ] + }, + "public.regional_group": { + "name": "regional_group", + "schema": "public", + "values": [ + "AFRICA", + "ASIA_PACIFIC", + "EASTERN_EUROPE", + "LATIN_AMERICA_CARIBBEAN", + "WESTERN_EUROPE_OTHERS" + ] + }, + "public.representation_type": { + "name": "representation_type", + "schema": "public", + "values": [ + "DELEGATION", + "NSA", + "UN" + ] + }, + "public.share_code_permission": { + "name": "share_code_permission", + "schema": "public", + "values": [ + "SPONSOR", + "EDIT" + ] + }, + "public.speakers_list_category": { + "name": "speakers_list_category", + "schema": "public", + "values": [ + "SPEAKERS_LIST", + "COMMENT_LIST" + ] + }, + "public.vote_outcome": { + "name": "vote_outcome", + "schema": "public", + "values": [ + "ADOPTED", + "REJECTED", + "SENT_BACK" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json new file mode 100644 index 00000000..9c2140ba --- /dev/null +++ b/drizzle/meta/0006_snapshot.json @@ -0,0 +1,2423 @@ +{ + "id": "bab6ce6b-1ddb-4477-aa04-11321db188a9", + "prevId": "f08afff5-d54b-4577-be17-3d085825de7d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agenda_item": { + "name": "agenda_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agenda_item_committee_id_committee_id_fk": { + "name": "agenda_item_committee_id_committee_id_fk", + "tableFrom": "agenda_item", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment": { + "name": "amendment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposer_committee_member_id": { + "name": "proposer_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "amendment_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "amendment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "target_clause_id": { + "name": "target_clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_operative_index": { + "name": "target_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "new_content": { + "name": "new_content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "target_position": { + "name": "target_position", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_paper_id_resolution_paper_id_fk": { + "name": "amendment_paper_id_resolution_paper_id_fk", + "tableFrom": "amendment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_proposer_committee_member_id_committee_member_id_fk": { + "name": "amendment_proposer_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment", + "tableTo": "committee_member", + "columnsFrom": [ + "proposer_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment_sponsor": { + "name": "amendment_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "amendment_id": { + "name": "amendment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_sponsor_amendment_id_amendment_id_fk": { + "name": "amendment_sponsor_amendment_id_amendment_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "amendment", + "columnsFrom": [ + "amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_sponsor_committee_member_id_committee_member_id_fk": { + "name": "amendment_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "amendment_sponsor_amendmentId_committeeMemberId_unique": { + "name": "amendment_sponsor_amendmentId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "amendment_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee": { + "name": "committee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whiteboard_content": { + "name": "whiteboard_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'

'" + }, + "show_whiteboard": { + "name": "show_whiteboard", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "committee_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SUSPENSION'" + }, + "status_headline": { + "name": "status_headline", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status_until": { + "name": "status_until", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "state_of_debate": { + "name": "state_of_debate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_delegations_to_add_themselves_to_speakers_list": { + "name": "allow_delegations_to_add_themselves_to_speakers_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_agenda_item_id": { + "name": "active_agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_simple_majority": { + "name": "custom_simple_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_two_thirds_majority": { + "name": "custom_two_thirds_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_paper_support_threshold": { + "name": "custom_paper_support_threshold", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "last_resolution_adoption_date": { + "name": "last_resolution_adoption_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_draft_resolutions": { + "name": "max_draft_resolutions", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "active_draft_resolution_id": { + "name": "active_draft_resolution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_operative_index": { + "name": "current_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "support_re_evaluation_open": { + "name": "support_re_evaluation_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "amendment_submission_open": { + "name": "amendment_submission_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "amendment_sponsoring_open": { + "name": "amendment_sponsoring_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "active_amendment_id": { + "name": "active_amendment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_headline": { + "name": "resolution_headline", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "committee_conference_id_conference_id_fk": { + "name": "committee_conference_id_conference_id_fk", + "tableFrom": "committee", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_active_agenda_item_id_agenda_item_id_fk": { + "name": "committee_active_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee", + "tableTo": "agenda_item", + "columnsFrom": [ + "active_agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_draft_resolution_id_resolution_paper_id_fk": { + "name": "committee_active_draft_resolution_id_resolution_paper_id_fk", + "tableFrom": "committee", + "tableTo": "resolution_paper", + "columnsFrom": [ + "active_draft_resolution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_amendment_id_amendment_id_fk": { + "name": "committee_active_amendment_id_amendment_id_fk", + "tableFrom": "committee", + "tableTo": "amendment", + "columnsFrom": [ + "active_amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "committee_conferenceId_name_unique": { + "name": "committee_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "committee_conferenceId_abbreviation_unique": { + "name": "committee_conferenceId_abbreviation_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "abbreviation" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_member": { + "name": "committee_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "present": { + "name": "present", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_member_committee_id_committee_id_fk": { + "name": "committee_member_committee_id_committee_id_fk", + "tableFrom": "committee_member", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_member_representation_id_representation_id_fk": { + "name": "committee_member_representation_id_representation_id_fk", + "tableFrom": "committee_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_topic_changed_timestamp": { + "name": "committee_topic_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_topic_changed_timestamp_committee_id_committee_id_fk": { + "name": "committee_topic_changed_timestamp_committee_id_committee_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk": { + "name": "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference": { + "name": "conference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "press_website": { + "name": "press_website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_moderated_caucus": { + "name": "has_moderated_caucus", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_member": { + "name": "conference_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "conference_member_conference_id_conference_id_fk": { + "name": "conference_member_conference_id_conference_id_fk", + "tableFrom": "conference_member", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_member_representation_id_representation_id_fk": { + "name": "conference_member_representation_id_representation_id_fk", + "tableFrom": "conference_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_user": { + "name": "conference_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_user_type": { + "name": "conference_user_type", + "type": "conference_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "conference_user_conference_id_conference_id_fk": { + "name": "conference_user_conference_id_conference_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_conference_member_id_conference_member_id_fk": { + "name": "conference_user_conference_member_id_conference_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_committee_member_id_committee_member_id_fk": { + "name": "conference_user_committee_member_id_committee_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operative_clause_vote": { + "name": "operative_clause_vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "operative_clause_vote_paper_id_resolution_paper_id_fk": { + "name": "operative_clause_vote_paper_id_resolution_paper_id_fk", + "tableFrom": "operative_clause_vote", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "operative_clause_vote_paperId_clauseId_unique": { + "name": "operative_clause_vote_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_clause_lock": { + "name": "paper_clause_lock", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "paper_clause_lock_paper_id_resolution_paper_id_fk": { + "name": "paper_clause_lock_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_clause_lock_conference_user_id_conference_user_id_fk": { + "name": "paper_clause_lock_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_clause_lock_paperId_clauseId_unique": { + "name": "paper_clause_lock_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_content_snapshot": { + "name": "paper_content_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "paper_content_snapshot_paper_id_resolution_paper_id_fk": { + "name": "paper_content_snapshot_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_content_snapshot", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_editor": { + "name": "paper_editor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_editor_paper_id_resolution_paper_id_fk": { + "name": "paper_editor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_editor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_editor_conference_user_id_conference_user_id_fk": { + "name": "paper_editor_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_editor", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_editor_paperId_conferenceUserId_unique": { + "name": "paper_editor_paperId_conferenceUserId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "conference_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_share_code": { + "name": "paper_share_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "share_code_permission", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_share_code_paper_id_resolution_paper_id_fk": { + "name": "paper_share_code_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_share_code", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_share_code_code_unique": { + "name": "paper_share_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_sponsor": { + "name": "paper_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_sponsor_paper_id_resolution_paper_id_fk": { + "name": "paper_sponsor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_sponsor_committee_member_id_committee_member_id_fk": { + "name": "paper_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_sponsor_paperId_committeeMemberId_unique": { + "name": "paper_sponsor_paperId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.presence_changed_timestamp": { + "name": "presence_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "present_set_to": { + "name": "present_set_to", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "presence_changed_timestamp_committee_member_id_committee_member_id_fk": { + "name": "presence_changed_timestamp_committee_member_id_committee_member_id_fk", + "tableFrom": "presence_changed_timestamp", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.representation": { + "name": "representation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha2_code": { + "name": "alpha2_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha3_code": { + "name": "alpha3_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "representation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fa_icon": { + "name": "fa_icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regional_group": { + "name": "regional_group", + "type": "regional_group", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "representation_conference_id_conference_id_fk": { + "name": "representation_conference_id_conference_id_fk", + "tableFrom": "representation", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "representation_conferenceId_name_unique": { + "name": "representation_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "representation_conferenceId_alpha2Code_alpha3Code_unique": { + "name": "representation_conferenceId_alpha2Code_alpha3Code_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "alpha2_code", + "alpha3_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_comment": { + "name": "resolution_comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_conference_user_id": { + "name": "author_conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "comment_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLIC'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_comment_paper_id_resolution_paper_id_fk": { + "name": "resolution_comment_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_author_conference_user_id_conference_user_id_fk": { + "name": "resolution_comment_author_conference_user_id_conference_user_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "conference_user", + "columnsFrom": [ + "author_conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_parent_comment_id_resolution_comment_id_fk": { + "name": "resolution_comment_parent_comment_id_resolution_comment_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_comment", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_paper": { + "name": "resolution_paper", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_committee_member_id": { + "name": "creator_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "paper_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'WORKING_PAPER'" + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_paper_committee_id_committee_id_fk": { + "name": "resolution_paper_committee_id_committee_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_agenda_item_id_agenda_item_id_fk": { + "name": "resolution_paper_agenda_item_id_agenda_item_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_creator_committee_member_id_committee_member_id_fk": { + "name": "resolution_paper_creator_committee_member_id_committee_member_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee_member", + "columnsFrom": [ + "creator_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_vote_result": { + "name": "resolution_vote_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_vote_result_paper_id_resolution_paper_id_fk": { + "name": "resolution_vote_result_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_vote_result", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resolution_vote_result_paperId_unique": { + "name": "resolution_vote_result_paperId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speaker_on_list": { + "name": "speaker_on_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "overwrite_name": { + "name": "overwrite_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "speaker_on_list_committee_member_id_committee_member_id_fk": { + "name": "speaker_on_list_committee_member_id_committee_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_conference_member_id_conference_member_id_fk": { + "name": "speaker_on_list_conference_member_id_conference_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_speakers_list_id_speakers_list_id_fk": { + "name": "speaker_on_list_speakers_list_id_speakers_list_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speaker_on_list_speakersListId_position_unique": { + "name": "speaker_on_list_speakersListId_position_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "position" + ] + }, + "speaker_on_list_speakersListId_committeeMemberId_unique": { + "name": "speaker_on_list_speakersListId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "committee_member_id" + ] + }, + "speaker_on_list_speakersListId_conferenceMemberId_unique": { + "name": "speaker_on_list_speakersListId_conferenceMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "conference_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speakers_list": { + "name": "speakers_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "speakers_list_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "speaking_time": { + "name": "speaking_time", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "time_left": { + "name": "time_left", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_closed": { + "name": "is_closed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "speakers_list_agenda_item_id_agenda_item_id_fk": { + "name": "speakers_list_agenda_item_id_agenda_item_id_fk", + "tableFrom": "speakers_list", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speakers_list_agendaItemId_type_unique": { + "name": "speakers_list_agendaItemId_type_unique", + "nullsNotDistinct": false, + "columns": [ + "agenda_item_id", + "type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spoken_time_period": { + "name": "spoken_time_period", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_timestamp": { + "name": "end_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "spoken_time_period_committee_member_id_committee_member_id_fk": { + "name": "spoken_time_period_committee_member_id_committee_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_conference_member_id_conference_member_id_fk": { + "name": "spoken_time_period_conference_member_id_conference_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_speakers_list_id_speakers_list_id_fk": { + "name": "spoken_time_period_speakers_list_id_speakers_list_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_username": { + "name": "preferred_username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_id_unique": { + "name": "user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + }, + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.amendment_status": { + "name": "amendment_status", + "schema": "public", + "values": [ + "PENDING", + "SUBMITTED", + "CONSENSUS_ADOPTED", + "ACCEPTED", + "REJECTED", + "WITHDRAWN" + ] + }, + "public.amendment_type": { + "name": "amendment_type", + "schema": "public", + "values": [ + "DELETE", + "ADD", + "ALTER_TEXT", + "ALTER_POSITION" + ] + }, + "public.comment_visibility": { + "name": "comment_visibility", + "schema": "public", + "values": [ + "PUBLIC", + "TEAM_ONLY" + ] + }, + "public.committee_status": { + "name": "committee_status", + "schema": "public", + "values": [ + "FORMAL", + "INFORMAL", + "MODERATED_INFORMAL", + "PAUSE", + "SUSPENSION" + ] + }, + "public.conference_user_type": { + "name": "conference_user_type", + "schema": "public", + "values": [ + "ADMIN", + "TEAM", + "SPECTATOR", + "DELEGATE", + "NON_STATE_ACTOR" + ] + }, + "public.paper_status": { + "name": "paper_status", + "schema": "public", + "values": [ + "WORKING_PAPER", + "SUBMITTED", + "DRAFT_RESOLUTION", + "AMENDMENT_PHASE", + "VOTING_PHASE", + "FINAL" + ] + }, + "public.regional_group": { + "name": "regional_group", + "schema": "public", + "values": [ + "AFRICA", + "ASIA_PACIFIC", + "EASTERN_EUROPE", + "LATIN_AMERICA_CARIBBEAN", + "WESTERN_EUROPE_OTHERS" + ] + }, + "public.representation_type": { + "name": "representation_type", + "schema": "public", + "values": [ + "DELEGATION", + "NSA", + "UN" + ] + }, + "public.share_code_permission": { + "name": "share_code_permission", + "schema": "public", + "values": [ + "SPONSOR", + "EDIT" + ] + }, + "public.speakers_list_category": { + "name": "speakers_list_category", + "schema": "public", + "values": [ + "SPEAKERS_LIST", + "COMMENT_LIST" + ] + }, + "public.vote_outcome": { + "name": "vote_outcome", + "schema": "public", + "values": [ + "ADOPTED", + "REJECTED", + "SENT_BACK" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 00000000..6138d64c --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,2436 @@ +{ + "id": "5381be8e-1f70-46f8-a855-c48c44161730", + "prevId": "bab6ce6b-1ddb-4477-aa04-11321db188a9", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agenda_item": { + "name": "agenda_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "agenda_item_committee_id_committee_id_fk": { + "name": "agenda_item_committee_id_committee_id_fk", + "tableFrom": "agenda_item", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment": { + "name": "amendment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposer_committee_member_id": { + "name": "proposer_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "amendment_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "amendment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "target_clause_id": { + "name": "target_clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_operative_index": { + "name": "target_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "new_content": { + "name": "new_content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "target_position": { + "name": "target_position", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_paper_id_resolution_paper_id_fk": { + "name": "amendment_paper_id_resolution_paper_id_fk", + "tableFrom": "amendment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_proposer_committee_member_id_committee_member_id_fk": { + "name": "amendment_proposer_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment", + "tableTo": "committee_member", + "columnsFrom": [ + "proposer_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.amendment_sponsor": { + "name": "amendment_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "amendment_id": { + "name": "amendment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "amendment_sponsor_amendment_id_amendment_id_fk": { + "name": "amendment_sponsor_amendment_id_amendment_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "amendment", + "columnsFrom": [ + "amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "amendment_sponsor_committee_member_id_committee_member_id_fk": { + "name": "amendment_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "amendment_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "amendment_sponsor_amendmentId_committeeMemberId_unique": { + "name": "amendment_sponsor_amendmentId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "amendment_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee": { + "name": "committee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whiteboard_content": { + "name": "whiteboard_content", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'

'" + }, + "show_whiteboard": { + "name": "show_whiteboard", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "committee_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'SUSPENSION'" + }, + "status_headline": { + "name": "status_headline", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status_until": { + "name": "status_until", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "state_of_debate": { + "name": "state_of_debate", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allow_delegations_to_add_themselves_to_speakers_list": { + "name": "allow_delegations_to_add_themselves_to_speakers_list", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_agenda_item_id": { + "name": "active_agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_simple_majority": { + "name": "custom_simple_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_two_thirds_majority": { + "name": "custom_two_thirds_majority", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "custom_paper_support_threshold": { + "name": "custom_paper_support_threshold", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "last_resolution_adoption_date": { + "name": "last_resolution_adoption_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_draft_resolutions": { + "name": "max_draft_resolutions", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "active_draft_resolution_id": { + "name": "active_draft_resolution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_operative_index": { + "name": "current_operative_index", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "current_operative_clause_id": { + "name": "current_operative_clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "support_re_evaluation_open": { + "name": "support_re_evaluation_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "amendment_submission_open": { + "name": "amendment_submission_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "amendment_sponsoring_open": { + "name": "amendment_sponsoring_open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "active_amendment_id": { + "name": "active_amendment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_headline": { + "name": "resolution_headline", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "committee_conference_id_conference_id_fk": { + "name": "committee_conference_id_conference_id_fk", + "tableFrom": "committee", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_active_agenda_item_id_agenda_item_id_fk": { + "name": "committee_active_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee", + "tableTo": "agenda_item", + "columnsFrom": [ + "active_agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_draft_resolution_id_resolution_paper_id_fk": { + "name": "committee_active_draft_resolution_id_resolution_paper_id_fk", + "tableFrom": "committee", + "tableTo": "resolution_paper", + "columnsFrom": [ + "active_draft_resolution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "committee_active_amendment_id_amendment_id_fk": { + "name": "committee_active_amendment_id_amendment_id_fk", + "tableFrom": "committee", + "tableTo": "amendment", + "columnsFrom": [ + "active_amendment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "committee_conferenceId_name_unique": { + "name": "committee_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "committee_conferenceId_abbreviation_unique": { + "name": "committee_conferenceId_abbreviation_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "abbreviation" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_member": { + "name": "committee_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "present": { + "name": "present", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_member_committee_id_committee_id_fk": { + "name": "committee_member_committee_id_committee_id_fk", + "tableFrom": "committee_member", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_member_representation_id_representation_id_fk": { + "name": "committee_member_representation_id_representation_id_fk", + "tableFrom": "committee_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.committee_topic_changed_timestamp": { + "name": "committee_topic_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "committee_topic_changed_timestamp_committee_id_committee_id_fk": { + "name": "committee_topic_changed_timestamp_committee_id_committee_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk": { + "name": "committee_topic_changed_timestamp_agenda_item_id_agenda_item_id_fk", + "tableFrom": "committee_topic_changed_timestamp", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference": { + "name": "conference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "press_website": { + "name": "press_website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_moderated_caucus": { + "name": "has_moderated_caucus", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "resolution_feature_enabled": { + "name": "resolution_feature_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_member": { + "name": "conference_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "representation_id": { + "name": "representation_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "conference_member_conference_id_conference_id_fk": { + "name": "conference_member_conference_id_conference_id_fk", + "tableFrom": "conference_member", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_member_representation_id_representation_id_fk": { + "name": "conference_member_representation_id_representation_id_fk", + "tableFrom": "conference_member", + "tableTo": "representation", + "columnsFrom": [ + "representation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.conference_user": { + "name": "conference_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "conference_user_type": { + "name": "conference_user_type", + "type": "conference_user_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "conference_user_conference_id_conference_id_fk": { + "name": "conference_user_conference_id_conference_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_conference_member_id_conference_member_id_fk": { + "name": "conference_user_conference_member_id_conference_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "conference_user_committee_member_id_committee_member_id_fk": { + "name": "conference_user_committee_member_id_committee_member_id_fk", + "tableFrom": "conference_user", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operative_clause_vote": { + "name": "operative_clause_vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "operative_clause_vote_paper_id_resolution_paper_id_fk": { + "name": "operative_clause_vote_paper_id_resolution_paper_id_fk", + "tableFrom": "operative_clause_vote", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "operative_clause_vote_paperId_clauseId_unique": { + "name": "operative_clause_vote_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_clause_lock": { + "name": "paper_clause_lock", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "paper_clause_lock_paper_id_resolution_paper_id_fk": { + "name": "paper_clause_lock_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_clause_lock_conference_user_id_conference_user_id_fk": { + "name": "paper_clause_lock_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_clause_lock", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_clause_lock_paperId_clauseId_unique": { + "name": "paper_clause_lock_paperId_clauseId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "clause_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_content_snapshot": { + "name": "paper_content_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "paper_content_snapshot_paper_id_resolution_paper_id_fk": { + "name": "paper_content_snapshot_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_content_snapshot", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_editor": { + "name": "paper_editor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conference_user_id": { + "name": "conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_editor_paper_id_resolution_paper_id_fk": { + "name": "paper_editor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_editor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_editor_conference_user_id_conference_user_id_fk": { + "name": "paper_editor_conference_user_id_conference_user_id_fk", + "tableFrom": "paper_editor", + "tableTo": "conference_user", + "columnsFrom": [ + "conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_editor_paperId_conferenceUserId_unique": { + "name": "paper_editor_paperId_conferenceUserId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "conference_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_share_code": { + "name": "paper_share_code", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "share_code_permission", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_share_code_paper_id_resolution_paper_id_fk": { + "name": "paper_share_code_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_share_code", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_share_code_code_unique": { + "name": "paper_share_code_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paper_sponsor": { + "name": "paper_sponsor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "paper_sponsor_paper_id_resolution_paper_id_fk": { + "name": "paper_sponsor_paper_id_resolution_paper_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "paper_sponsor_committee_member_id_committee_member_id_fk": { + "name": "paper_sponsor_committee_member_id_committee_member_id_fk", + "tableFrom": "paper_sponsor", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "paper_sponsor_paperId_committeeMemberId_unique": { + "name": "paper_sponsor_paperId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id", + "committee_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.presence_changed_timestamp": { + "name": "presence_changed_timestamp", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "present_set_to": { + "name": "present_set_to", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "presence_changed_timestamp_committee_member_id_committee_member_id_fk": { + "name": "presence_changed_timestamp_committee_member_id_committee_member_id_fk", + "tableFrom": "presence_changed_timestamp", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.representation": { + "name": "representation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha2_code": { + "name": "alpha2_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "alpha3_code": { + "name": "alpha3_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "representation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fa_icon": { + "name": "fa_icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "regional_group": { + "name": "regional_group", + "type": "regional_group", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "conference_id": { + "name": "conference_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "representation_conference_id_conference_id_fk": { + "name": "representation_conference_id_conference_id_fk", + "tableFrom": "representation", + "tableTo": "conference", + "columnsFrom": [ + "conference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "representation_conferenceId_name_unique": { + "name": "representation_conferenceId_name_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "name" + ] + }, + "representation_conferenceId_alpha2Code_alpha3Code_unique": { + "name": "representation_conferenceId_alpha2Code_alpha3Code_unique", + "nullsNotDistinct": false, + "columns": [ + "conference_id", + "alpha2_code", + "alpha3_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_comment": { + "name": "resolution_comment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clause_id": { + "name": "clause_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_conference_user_id": { + "name": "author_conference_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "comment_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PUBLIC'" + }, + "parent_comment_id": { + "name": "parent_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_comment_paper_id_resolution_paper_id_fk": { + "name": "resolution_comment_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_author_conference_user_id_conference_user_id_fk": { + "name": "resolution_comment_author_conference_user_id_conference_user_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "conference_user", + "columnsFrom": [ + "author_conference_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_comment_parent_comment_id_resolution_comment_id_fk": { + "name": "resolution_comment_parent_comment_id_resolution_comment_id_fk", + "tableFrom": "resolution_comment", + "tableTo": "resolution_comment", + "columnsFrom": [ + "parent_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_paper": { + "name": "resolution_paper", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_id": { + "name": "committee_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creator_committee_member_id": { + "name": "creator_committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "paper_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'WORKING_PAPER'" + }, + "content": { + "name": "content", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_number": { + "name": "document_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sequence_number": { + "name": "sequence_number", + "type": "smallint", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_paper_committee_id_committee_id_fk": { + "name": "resolution_paper_committee_id_committee_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee", + "columnsFrom": [ + "committee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_agenda_item_id_agenda_item_id_fk": { + "name": "resolution_paper_agenda_item_id_agenda_item_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resolution_paper_creator_committee_member_id_committee_member_id_fk": { + "name": "resolution_paper_creator_committee_member_id_committee_member_id_fk", + "tableFrom": "resolution_paper", + "tableTo": "committee_member", + "columnsFrom": [ + "creator_committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resolution_vote_result": { + "name": "resolution_vote_result", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "paper_id": { + "name": "paper_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "vote_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "votes_for": { + "name": "votes_for", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_against": { + "name": "votes_against", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "votes_abstain": { + "name": "votes_abstain", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "resolution_vote_result_paper_id_resolution_paper_id_fk": { + "name": "resolution_vote_result_paper_id_resolution_paper_id_fk", + "tableFrom": "resolution_vote_result", + "tableTo": "resolution_paper", + "columnsFrom": [ + "paper_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resolution_vote_result_paperId_unique": { + "name": "resolution_vote_result_paperId_unique", + "nullsNotDistinct": false, + "columns": [ + "paper_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speaker_on_list": { + "name": "speaker_on_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "overwrite_name": { + "name": "overwrite_name", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "speaker_on_list_committee_member_id_committee_member_id_fk": { + "name": "speaker_on_list_committee_member_id_committee_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_conference_member_id_conference_member_id_fk": { + "name": "speaker_on_list_conference_member_id_conference_member_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speaker_on_list_speakers_list_id_speakers_list_id_fk": { + "name": "speaker_on_list_speakers_list_id_speakers_list_id_fk", + "tableFrom": "speaker_on_list", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speaker_on_list_speakersListId_position_unique": { + "name": "speaker_on_list_speakersListId_position_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "position" + ] + }, + "speaker_on_list_speakersListId_committeeMemberId_unique": { + "name": "speaker_on_list_speakersListId_committeeMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "committee_member_id" + ] + }, + "speaker_on_list_speakersListId_conferenceMemberId_unique": { + "name": "speaker_on_list_speakersListId_conferenceMemberId_unique", + "nullsNotDistinct": false, + "columns": [ + "speakers_list_id", + "conference_member_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speakers_list": { + "name": "speakers_list", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "agenda_item_id": { + "name": "agenda_item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "speakers_list_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "speaking_time": { + "name": "speaking_time", + "type": "smallint", + "primaryKey": false, + "notNull": true + }, + "time_left": { + "name": "time_left", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_closed": { + "name": "is_closed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "speakers_list_agenda_item_id_agenda_item_id_fk": { + "name": "speakers_list_agenda_item_id_agenda_item_id_fk", + "tableFrom": "speakers_list", + "tableTo": "agenda_item", + "columnsFrom": [ + "agenda_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "speakers_list_agendaItemId_type_unique": { + "name": "speakers_list_agendaItemId_type_unique", + "nullsNotDistinct": false, + "columns": [ + "agenda_item_id", + "type" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.spoken_time_period": { + "name": "spoken_time_period", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "committee_member_id": { + "name": "committee_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "conference_member_id": { + "name": "conference_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "speakers_list_id": { + "name": "speakers_list_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_timestamp": { + "name": "start_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "end_timestamp": { + "name": "end_timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "spoken_time_period_committee_member_id_committee_member_id_fk": { + "name": "spoken_time_period_committee_member_id_committee_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "committee_member", + "columnsFrom": [ + "committee_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_conference_member_id_conference_member_id_fk": { + "name": "spoken_time_period_conference_member_id_conference_member_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "conference_member", + "columnsFrom": [ + "conference_member_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "spoken_time_period_speakers_list_id_speakers_list_id_fk": { + "name": "spoken_time_period_speakers_list_id_speakers_list_id_fk", + "tableFrom": "spoken_time_period", + "tableTo": "speakers_list", + "columnsFrom": [ + "speakers_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preferred_username": { + "name": "preferred_username", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_id_unique": { + "name": "user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + }, + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.amendment_status": { + "name": "amendment_status", + "schema": "public", + "values": [ + "PENDING", + "SUBMITTED", + "CONSENSUS_ADOPTED", + "ACCEPTED", + "REJECTED", + "WITHDRAWN" + ] + }, + "public.amendment_type": { + "name": "amendment_type", + "schema": "public", + "values": [ + "DELETE", + "ADD", + "ALTER_TEXT", + "ALTER_POSITION" + ] + }, + "public.comment_visibility": { + "name": "comment_visibility", + "schema": "public", + "values": [ + "PUBLIC", + "TEAM_ONLY" + ] + }, + "public.committee_status": { + "name": "committee_status", + "schema": "public", + "values": [ + "FORMAL", + "INFORMAL", + "MODERATED_INFORMAL", + "PAUSE", + "SUSPENSION" + ] + }, + "public.conference_user_type": { + "name": "conference_user_type", + "schema": "public", + "values": [ + "ADMIN", + "TEAM", + "SPECTATOR", + "DELEGATE", + "NON_STATE_ACTOR" + ] + }, + "public.paper_status": { + "name": "paper_status", + "schema": "public", + "values": [ + "WORKING_PAPER", + "SUBMITTED", + "DRAFT_RESOLUTION", + "AMENDMENT_PHASE", + "VOTING_PHASE", + "FINAL" + ] + }, + "public.regional_group": { + "name": "regional_group", + "schema": "public", + "values": [ + "AFRICA", + "ASIA_PACIFIC", + "EASTERN_EUROPE", + "LATIN_AMERICA_CARIBBEAN", + "WESTERN_EUROPE_OTHERS" + ] + }, + "public.representation_type": { + "name": "representation_type", + "schema": "public", + "values": [ + "DELEGATION", + "NSA", + "UN" + ] + }, + "public.share_code_permission": { + "name": "share_code_permission", + "schema": "public", + "values": [ + "SPONSOR", + "EDIT" + ] + }, + "public.speakers_list_category": { + "name": "speakers_list_category", + "schema": "public", + "values": [ + "SPEAKERS_LIST", + "COMMENT_LIST" + ] + }, + "public.vote_outcome": { + "name": "vote_outcome", + "schema": "public", + "values": [ + "ADOPTED", + "REJECTED", + "SENT_BACK" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8de0e8ee..21ba6f1e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,41 +1,62 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1747766528550, - "tag": "0000_chemical_network", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1747942198735, - "tag": "0001_romantic_shape", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1748455384043, - "tag": "0002_panoramic_agent_brand", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1750785323213, - "tag": "0003_mighty_maestro", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1750964889620, - "tag": "0004_orange_vengeance", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1747766528550, + "tag": "0000_chemical_network", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1747942198735, + "tag": "0001_romantic_shape", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1748455384043, + "tag": "0002_panoramic_agent_brand", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1750785323213, + "tag": "0003_mighty_maestro", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1750964889620, + "tag": "0004_orange_vengeance", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1773013488762, + "tag": "0005_tan_paibok", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1773189153562, + "tag": "0006_cuddly_sersi", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1775228132434, + "tag": "0007_typical_the_renegades", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml index f8a5fa66..b1e0d3d2 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -8,3 +8,5 @@ pre-push: commands: lint: run: bunx eslint "{push_files}" + i18n: + run: bun run i18n:check diff --git a/messages/de.json b/messages/de.json index 06b97cfe..304f7022 100644 --- a/messages/de.json +++ b/messages/de.json @@ -5,22 +5,74 @@ "absent": "Abwesend", "absoluteMajority": "Absolut", "abstain": "Enthaltung", + "activeAmendment": "In Behandlung", + "activeDraftResolution": "In Behandlung", "addAgendaItem": "Punkt hinzufügen", "addAll": "Alle hinzufügen", + "addClause": "Absatz hinzufügen", + "addClausePresentation": "Absatz hinzufügen", + "addComment": "Kommentar hinzufügen", "addCommittee": "Gremium hinzufügen", "addCountriesCount": "{count} Länder hinzufügen", "addCountry": "Land hinzufügen", + "addMeToList": "Auf die Liste setzen", "addMember": "Mitglied hinzufügen", "addNonStateActor": "NA hinzufügen", + "addRepresentation": "Delegation hinzufügen", + "addSponsor": "Sponsor hinzufügen", "addUnActor": "UN-Akteur hinzufügen", "admin": "Admin", + "adoptByConsensus": "Per Konsens annehmen", + "adoptClause": "Annehmen", + "adoptResolution": "Resolution annehmen", + "adopted": "Angenommen", "adoptionAnnouncement": "BREAKING: Verabschiedung einer Resolution zum Thema \"{agendaItem}\" im Gremium {committeeName}", + "advanceToNextParagraph": "Nächster Absatz", "agendaItem": "Tagesordnungspunkt", "agendaItemTitle": "Titel des Tagesordnungspunkts", "agendaItems": "Tagesordnungspunkte", "allRightsReservedby": "Alle Rechte vorbehalten von", + "allowSelfAddToSpeakersList": "Selbst auf Redeliste setzen", + "allowSelfAddToSpeakersListDescription": "Delegierten und nichtstaatlichen Akteuren erlauben, sich selbst auf Redelisten zu setzen.", + "alterClausePresentation": "Text ändern", + "alterPosition": "Absatz verschieben", + "alterText": "Text ändern", + "amendment": "Änderungsantrag", + "amendmentAccepted": "Angenommen", + "amendmentAcceptedToast": "Änderungsantrag angenommen", + "amendmentAdd": "Absatz hinzufügen", + "amendmentAdopted": "Änderungsantrag angenommen", + "amendmentAlterPosition": "Position ändern", + "amendmentAlterText": "Text ändern", + "amendmentConsensusAdopted": "Per Konsens angenommen", + "amendmentCreated": "Änderungsantrag gestellt", + "amendmentDelete": "Absatz streichen", + "amendmentPending": "Ausstehend", + "amendmentPhase": "Änderungsantragsphase", + "amendmentPhaseActive": "Änderungsantragsphase aktiv", + "amendmentPhaseStarted": "Änderungsantragsphase gestartet", + "amendmentProposed": "Änderungsantrag vorgeschlagen", + "amendmentQueue": "Änderungsanträge", + "amendmentRejected": "Abgelehnt", + "amendmentRejectedClause": "Abgelehnt", + "amendmentRejectedToast": "Änderungsantrag abgelehnt", + "amendmentSponsoring": "Änderungsanträge unterstützen", + "amendmentSponsoringClosed": "Unterstützung von Änderungsanträgen ist geschlossen", + "amendmentSponsoringOpen": "Delegierte können Änderungsanträge unterstützen", + "amendmentSubmission": "Änderungsanträge einreichen", + "amendmentSubmissionClosed": "Einreichung von Änderungsanträgen ist geschlossen", + "amendmentSubmissionOpen": "Delegierte können neue Änderungsanträge einreichen", + "amendmentSubmitted": "Eingereicht", + "amendmentSubmittedToast": "Änderungsantrag eingereicht", + "amendmentUpdated": "Antrag aktualisiert", + "amendmentWithdrawn": "Zurückgezogen", + "amendmentWithdrawnToast": "Änderungsantrag zurückgezogen", + "amendments": "Änderungsanträge", "announceAdoption": "Verabschiedung verkünden", + "assignedCount": "{count} zugewiesen", + "assignment": "Zuweisung", "back": "Zurück", + "backToResolutions": "Zurück zu Resolutionen", "baseFontSize": "Basis-Schriftgröße", "baseFontSizeDescription": "Hier kann die Basis-Schriftgröße für die Präsentationsansicht festgelegt werden", "blockquote": "Zitat", @@ -28,18 +80,39 @@ "bulkAddMembers": "Mehrere Mitglieder hinzufügen", "bulkEmailPlaceholder": "E-Mail-Adressen eingeben (eine pro Zeile oder kommagetrennt)", "bulletedList": "Aufzählungsliste", + "cancel": "Abbrechen", "chairControls": "Vorsitz-Steuerung", + "chairCreateAmendment": "Änderungsantrag erstellen", + "chairCreateWorkingPaper": "Arbeitspapier erstellen", "changeSpeakersName": "Name ändern", "changeSpeakersTime": "Redezeit ändern", + "changesSaved": "Gespeichert", + "clauseComments": "auf Absätzen", + "clauseLockedBy": "Wird bearbeitet von {country}", + "clauseVoteDeleted": "Absatzstimmung entfernt", + "clauseVoteRecorded": "Absatzstimmung erfasst", + "clauseVoteSummary": "Absatzstimmungen-Übersicht", + "clausesVoted": "{voted}/{total} Absätze abgestimmt", + "clearActiveDr": "Aktiv aufheben", "clearFormatting": "Formatierung löschen", "clearList": "Liste zurücksetzen", "clearListDescription": "Bist du dir sicher, dass du die gesamte Liste zurücksetzen möchtest?", "close": "Schließen", "closeList": "Liste schließen", + "closeReEvaluation": "Änderung sperren", "code": "Code", + "codeCopied": "Code kopiert!", + "codeRedeemed": "Code erfolgreich eingelöst", "codesUnrecognized": "{count} Codes nicht erkannt", + "collaborativeEditingInfo": "Andere Delegierte bearbeiten diese Resolution. Fahre über einen Absatz und klicke \"Bearbeitung starten\" um zu beginnen. Sperren laufen nach 1 Minute Inaktivität automatisch ab.", "comingSoon": "bald verfügbar", + "commentDeleted": "Kommentar gelöscht", "commentList": "Fragen und Kurzbemerkungen", + "commentPlaceholder": "Kommentar schreiben...", + "commentPosted": "Kommentar gesendet", + "commentUpdated": "Kommentar aktualisiert", + "comments": "Kommentare", + "commentsOnClause": "{count} Kommentar(e)", "committee": "Gremium", "committeeAbbreviation": "Gremien-Abkürzung", "committeeDoesNotExist": "Das Gremium existiert nicht.", @@ -50,6 +123,7 @@ "committeeOverview": "Gremienübersicht", "committeeStatus": "Gremienstatus", "committeeStatusExpired": "{status} abgelaufen!", + "committees": "Gremien", "con": "Dagegen", "conferenceCreated": "Konferenz erstellt!", "conferenceCreationError": "Konferenz konnte nicht erstellt werden", @@ -58,7 +132,24 @@ "conferenceMembers": "Konferenzmitglieder", "conferenceTitle": "Konferenztitel", "configuration": "Konfiguration", + "confirmAdoptByConsensus": "Diesen Änderungsantrag per Konsens annehmen? Die Änderung wird sofort angewendet.", + "confirmAdoptResolution": "Diese Resolution annehmen? Konfetti feiert die Annahme!", + "confirmDeleteAmendment": "Möchtest du wirklich die Streichung dieses Absatzes beantragen?", + "confirmDeleteCommittee": "Möchtest du dieses Gremium wirklich löschen? Alle zugehörigen Daten gehen verloren.", + "confirmDeletePaper": "Dieses Arbeitspapier wirklich löschen? Es wird für alle Beteiligten ausgeblendet.", + "confirmDeleteRepresentation": "Möchtest du diese Delegation wirklich entfernen? Zugehörige Gremienmitgliedschaften werden entfernt.", + "confirmFinalVote": "Diese Schlussabstimmung erfassen? Der Resolutionsstatus wird auf Final gesetzt.", + "confirmRejectAmendment": "Diesen Änderungsantrag ablehnen?", + "confirmRejectResolution": "Diese Resolution ablehnen?", "confirmRemoveMember": "Möchtest du dieses Mitglied wirklich entfernen?", + "confirmRevertStatus": "Dieses Papier von {from} auf {to} zurücksetzen?", + "confirmSendBack": "Diese Resolution zurückverweisen?", + "confirmStartAmendmentPhase": "Änderungsantragsphase für diesen Resolutionsentwurf starten? Delegierte können dann absatzweise Änderungsanträge stellen.", + "confirmStartVotingPhase": "Die Abstimmungsphase für diese Beschlussvorlage starten? Jeder operative Absatz wird einzeln abgestimmt.", + "confirmSubmitPaper": "Möchtest du dieses Papier wirklich an den Vorsitz einreichen? Du kannst es danach nicht mehr bearbeiten.", + "copy": "Kopieren", + "copyCode": "Kopieren", + "copyFailed": "Kopieren fehlgeschlagen", "countries": "Länder", "countriesRecognized": "{count} Länder erkannt", "countryCodesHelp": "Unterstützt Alpha-2 (DE, US) und Alpha-3 (DEU, USA) Codes. Trenne mit Leerzeichen, Kommas, Semikolons oder neuen Zeilen.", @@ -67,28 +158,66 @@ "countryNsaOrCustomRole": "Land, NA oder spezielle Rolle", "create": "Erstellen", "createConference": "Konferenz erstellen", + "createPaper": "Papier erstellen", + "createResolutionPaper": "Arbeitspapier erstellen", + "createShareCodeEdit": "Bearbeitungs-Code erstellen", + "createShareCodeSponsor": "Unterstützer-Code erstellen", + "currentParagraph": "Aktueller Absatz", + "currentText": "Aktueller Text", "customName": "Benutzerdefinierter Name...", "dateCannotBeInPast": "Das Datum darf nicht in der Vergangenheit liegen!", + "debateControls": "Debattensteuerung", "delegate": "Delegierte*r", "delegations": "Delegationen", + "deleteClause": "Absatz streichen", + "deleteClausePresentation": "Absatz streichen", + "deleteCode": "Code löschen", + "deleteComment": "Löschen", + "deleteConference": "Konferenz löschen", + "deleteConferenceConfirmation": "Geben Sie den Konferenznamen ein, um das Löschen zu bestätigen:", + "deleteConferenceWarning": "Diese Aktion ist unwiderruflich. Alle Daten dieser Konferenz werden dauerhaft gelöscht.", + "deletePaper": "Papier löschen", + "deleteRepresentation": "Delegation entfernen", "displayRegionalGroups": "Regionalgruppenanzeige", + "documentLevelComments": "Dokumentkommentare", + "documentNumber": "Dokumentennummer", + "documentWide": "dokumentweit", + "doneEditing": "Fertig", "download": "Download", "downloadPresenceData": "Anwesenheitsdaten", + "draftResolution": "Resolutionsentwurf", + "draftResolutions": "Resolutionsentwürfe", "edit": "Bearbeiten", + "editAccess": "Bearbeitungszugriff", + "editAmendment": "Antrag bearbeiten", + "editComment": "Bearbeiten", + "editPaper": "Papier bearbeiten", + "editUser": "Benutzer bearbeiten", + "editors": "Bearbeiter", "email": "E-Mail", "enterAlpha2Code": "Bitte Alpha2Code eingeben", + "enterCode": "Code eingeben", "enterCountryCodes": "Ländercodes eingeben (Alpha-2 oder Alpha-3)", "errorUpdatingStateOfDebate": "Fehler beim Speichern des Debattenstatus", "errorUpdatingStatus": "Status konnte nicht gesetzt werden", "errorUpdatingTimer": "Redezeit konnte nicht aktualisiert werden", "errorUpdatingWhiteboard": "Veröffentlichung fehlgeschlagen", "fileParseError": "Fehler beim Parsen der Datei", + "finalResolution": "Endgültige Resolution", + "finalVote": "Schlussabstimmung", + "finalVoteDescription": "Die Schlussabstimmung über die gesamte Resolution erfassen.", + "finishAmendmentPhaseFirst": "Bitte zuerst die Änderungsphase abschließen", "formalDebate": "Formale Debatte", "forward": "Weiter", + "general": "Allgemein", + "goToAmendments": "Zu Änderungsanträgen", + "goToVoting": "Zur Abstimmung", "gotoSettings": "Zu den Einstellungen", "h1": "Überschrift 1", "h2": "Überschrift 2", "h3": "Überschrift 3", + "hasModeratedCaucus": "Moderierte informelle Sitzung", + "hasModeratedCaucusDescription": "Moderierte informelle Sitzung als Gremien-Status aktivieren.", "home": "Home", "homeAboutText": "CHASE (CHAirSoftwarE) ist eine Webanwendung zur Verwaltung und Durchführung von Debatten in Model United Nations Konferenzen. Sie ist für Vorsitzende und Delegierte gleichermaßen konzipiert. CHASE ermöglicht es Vorsitzenden, Debatten einfach zu verwalten, während Delegierte der Debatte folgen und mit anderen Delegierten auf intuitive und strukturierte Weise zusammenarbeiten können. CHASE ist freie und open source Software.", "homeAboutTitle": "Über CHASE", @@ -116,34 +245,38 @@ "importFromDelegator": "Konferenz aus Delegator importieren", "imprintAndPrivacy": "Impressum & Datenschutz", "informalCaucus": "Informelle Sitzung", + "insertAfterPresentation": "Einfügen nach OP.{index}", + "insertAsFirstClause": "Als ersten Absatz einfügen", + "insertAtBeginning": "Am Anfang einfügen", + "invalidShareCode": "Ungültiger Freigabecode", "italic": "Kursiv", + "language": "Sprache", "launcher": "Launcher", "launcherDescription": "Wähle die Konferenz aus", "launcherNoConferences": "Du bist für keine Konferenz angemeldet.", "launcherWelcome": "Willkommen zurück, {name}!", "layout": "Layout", "layoutDescription": "Layout-Vorlagen für die Präsentationsansicht. Bitte beachte, dass du manuelle Änderungen am Layout überschreibst, wenn du die Layoutvorlage hier änderst.", - "layoutPreset": [ - { - "declarations": ["input preset"], - "match": { - "preset=*": "Unbekanntes Layout", - "preset=default": "Standard Layout", - "preset=smallScreen": "Layout für kleine Bildschirme" - }, - "selectors": ["preset"] - } - ], + "layoutPresetDefault": "Standard Layout", + "layoutPresetResolution": "Resolutions-Layout", + "layoutPresetSmallScreen": "Layout für kleine Bildschirme", "layoutSelect": "Layout auswählen", "link": "Link", "listClosed": "Liste geschlossen", + "listClosedCannotAdd": "Die Liste ist geschlossen", "listEmpty": "Keine Rede", + "lockAcquireFailed": "Dieser Absatz wird gerade von {country} bearbeitet. Bitte versuche es gleich erneut.", "login": "Anmelden", "logout": "Abmelden", "loose_slow_reindeer_build": "Gremienmitglieder", + "majorities": "Mehrheiten", "majoritySettings": "Mehrheitseinstellungen", "majoritySettingsDescriptions": "Die Einstellung der Mehrheitsverhältnisse dient der richtigen Darstellung, ob eine Mehrheit erreicht wurde.", + "manageConferences": "Konferenzen verwalten", "maroon_bland_ray_renew": "Gremien-Abkürzung", + "matching": "übereinstimmend", + "maxDraftResolutions": "Max. Resolutionsentwürfe", + "maxDraftResolutionsReached": "Maximale Anzahl an Resolutionsentwürfen erreicht", "member": "Mitglied", "memberAdded": "Mitglied erfolgreich hinzugefügt", "memberRemoved": "Mitglied erfolgreich entfernt", @@ -152,32 +285,87 @@ "minutesFromNow": "Relative Zeit: Springe X Minuten in die Zukunft", "missionControl": "Mission Control", "moderatedInformalCaucus": "Moderierte informelle Sitzung", + "moveClausePresentation": "Absatz verschieben", + "moveToPositionPresentation": "Verschieben an Position {position}", + "myAmendments": "Meine Änderungsanträge", + "myPapers": "Meine Papiere", + "name": "Name", + "nextParagraph": "Weiter", "nextSpeaker": "Nächste Rede", "nextSpeakerDescription": "Möchtest du wirklich die nächste Rede aufrufen? Eventuelle Fragen- und Kurzbemerkungen werden verfallen.", + "noActiveAgendaItem": "Kein aktiver Tagesordnungspunkt. Es kann derzeit kein Papier erstellt werden.", + "noActiveDr": "Kein aktiver Resolutionsentwurf", + "noActiveDrForVoting": "Keine aktive Beschlussvorlage für Abstimmung", + "noActiveDraftResolution": "Kein aktiver Resolutionsentwurf", "noAgendaItemSelected": "Kein Tagesordnungspunkt aktiv", "noAgendaItemSelectedDescription": "Um mit Redelisten arbeiten zu können muss zunächst ein Tagesordnungspunkt ausgewählt werden", + "noAmendments": "Noch keine Änderungsanträge", + "noAssignmentNeeded": "Keine Mitgliedszuweisung für diese Rolle nötig.", "noCommentList": "Keine Liste für Fragen und Kurzbemerkungen", + "noComments": "Noch keine Kommentare", "noCurrentSpeaker": "Keine Rede", "noData": "Keine Daten", + "noDraftResolution": "Kein Resolutionsentwurf als aktiv gesetzt.", + "noDraftResolutionsYet": "Noch keine Resolutionsentwürfe.", "noMembers": "Noch keine Mitglieder", + "noOperativeClauses": "Keine operativen Absätze", + "noPapersYet": "Noch keine Papiere. Erstelle eines oder gib einen Freigabecode ein.", "noResults": "Keine Ergebnisse", + "noSubmittedPapers": "Noch keine eingereichten Papiere.", "nonStateActor": "Nichtstaatlicher Akteur", "nonStateActors": "Nichtstaatliche Akteure", "notAuthorized": "Du bist nicht berechtigt, auf diese Seite zuzugreifen", "notPresent": "Nicht anwesend", + "notPresentCannotAdd": "Du musst als anwesend markiert sein", "nothingChanged": "Nichts verändert", "numberedList": "Nummerierte Liste", "off": "Aus", "on": "An", + "onListPosition": "Du bist #{position} auf der Liste", "openPresentation": "Präsentationsansicht öffnen", + "openReEvaluation": "Änderung erlauben", + "operativeClause": "Operativer Absatz", + "operativeClausePresentation": "Operativer Absatz", + "outcome": "Ergebnis", + "over": "drüber", + "paperCreated": "Papier erstellt", + "paperDeleted": "Papier gelöscht", + "paperPromoted": "Papier zum Resolutionsentwurf befördert", + "paperSubmitted": "Papier beim Vorsitz eingereicht", + "paperSupportThresholdTooltip": "Benötigte Unterstützerstaaten für das Einreichen eines Änderungsantrags", + "paperTitle": "Papiertitel", + "papers": "Papiere", + "paragraphVoting": "Absatzweise Abstimmung", "parsedCountries": "Hinzuzufügende Länder:", + "participantView": "Teilnehmeransicht", "pause": "Pause", + "phraseCopied": "Operator kopiert!", + "phraseLookup": "Operatoren", + "phraseLookupDisclaimer": "Diese Operatoren sind als Orientierung gedacht. Bitte überprüfen Sie die korrekte Verwendung im Kontext.", + "phraseLookupNoResults": "Keine Operatoren gefunden.", + "phraseLookupSearch": "Operator suchen...", + "phraseLookupTitle": "Operatoren-Nachschlagewerk", + "preambleClause": "Präambelabsatz", "presence": "Anwesenheit", "present": "Anwesend", "presentationMode": "Präsentationsansicht", + "pressWebsite": "Presse-Website", + "preview": "Vorschau", + "previousParagraph": "Zurück", + "printResolution": "Drucken", "pro": "Dafür", + "promote": "Befördern", + "promoteToDraftResolution": "Zum Resolutionsentwurf befördern", + "promoteToDraftResolutionConfirm": "Dieses Papier zum Resolutionsentwurf befördern? Es wird eine Dokumentennummer zugewiesen.", + "proposeAmendment": "Änderungsantrag stellen", + "proposedAmendmentPresentation": "Vorgeschlagener Änderungsantrag", + "proposedBy": "Vorgeschlagen von {name}", + "proposedText": "Vorgeschlagener Text", + "publicComment": "Öffentlich", "publish": "Veröffentlichen", "publishChanges": "Änderungen Veröffentlichen", + "recordVoteFromVoting": "Stimmung erfassen", + "redeemShareCode": "Freigabecode einlösen", "redo": "Wiederholen", "regionalGroup_africa": "Afrika", "regionalGroup_asiaPacific": "Asien", @@ -185,7 +373,82 @@ "regionalGroup_latinAmericaCaribbean": "Lateinamerika und Karibik", "regionalGroup_westernEuropeOthers": "Westeuropa und Andere", "regionalGroups": "Regionalgruppen", + "rejectClause": "Ablehnen", + "rejectResolution": "Resolution ablehnen", + "rejected": "Abgelehnt", + "removeFromList": "Von der Liste entfernen", "removeMember": "Entfernen", + "removeSponsor": "Unterstützung zurückziehen", + "replyToComment": "Antworten", + "resolution": "Resolution", + "resolutionAddClause": "Absatz hinzufügen", + "resolutionAddContinuation": "Fortsetzungstext", + "resolutionAddFirstClause": "Ersten Absatz hinzufügen", + "resolutionAddNested": "Verschachtelter Absatz", + "resolutionAddSibling": "Absatz hinzufügen", + "resolutionAddSubClause": "Unterabsatz", + "resolutionAdopted": "Resolution angenommen!", + "resolutionAuthoringDelegation": "Einreichende Delegation", + "resolutionCommittee": "Gremium", + "resolutionContinuationPlaceholder": "Fortsetzungstext eingeben...", + "resolutionDeleteBlock": "Block löschen", + "resolutionDeleteClause": "Löschen", + "resolutionDisclaimer": "Dieses Dokument wurde im Rahmen einer {conferenceName}-Simulation erstellt und besitzt keine rechtliche Gültigkeit.", + "resolutionEditor": "Resolutions-Editor", + "resolutionFeatureEnabled": "Resolutionsfunktionen", + "resolutionFeatureEnabledDescription": "Resolutionseditor, Arbeitspapiere und Änderungsanträge für diese Konferenz aktivieren.", + "resolutionFontSize": "Resolutions-Schriftgröße", + "resolutionFontSizeDescription": "Hier kann die Schriftgröße für den Resolutionstext in der Präsentationsansicht festgelegt werden.", + "resolutionHeadline": "Resolutions-Kopfzeile (z.B. Der Sicherheitsrat)", + "resolutionHidePreview": "Vorschau ausblenden", + "resolutionImport": "Importieren", + "resolutionImportButton": "{count} Absätze importieren", + "resolutionImportHintOperative": "Fügen Sie nummerierte operative Absätze ein. Unterpunkte werden automatisch erkannt.", + "resolutionImportHintPreamble": "Fügen Sie Präambelabsätze ein, getrennt durch Komma und Zeilenumbruch.", + "resolutionImportLLMCopied": "Kopiert!", + "resolutionImportLLMCopyPrompt": "Prompt kopieren", + "resolutionImportLLMInstructions": "Kopieren Sie den folgenden Prompt in einen KI-Assistenten, um Ihren Text automatisch formatieren zu lassen:", + "resolutionImportLLMPromptOperative": "Formatiere den folgenden Text als operative Absätze einer UN-Resolution. Verwende:\n- Nummerierung für Hauptabsätze: 1. 2. 3.\n- Buchstaben für Unterabsätze: a) b) c)\n- Römische Ziffern für weitere Verschachtelung: i) ii) iii)\n- Doppelbuchstaben für tiefste Ebene: aa) bb) cc)\n- Semikolon am Ende jedes Absatzes, Punkt am Ende des letzten\n\nBeispielformat:\n1. fordert alle Mitgliedstaaten auf, Maßnahmen zu ergreifen;\n a) zur Förderung des Friedens;\n b) zur Stärkung der Zusammenarbeit;\n i) auf bilateraler Ebene;\n ii) auf multilateraler Ebene;\n2. bittet den Generalsekretär, einen Bericht vorzulegen.\n\nZu formatierender Text:", + "resolutionImportLLMPromptPreamble": "Formatiere den folgenden Text als Präambelabsätze einer UN-Resolution. Jeder Absatz sollte:\n- Mit einem Kleinbuchstaben beginnen (außer Eigennamen)\n- Mit einem Komma enden\n- Durch einen Zeilenumbruch getrennt sein\n\nBeispielformat:\nin Anbetracht der Notwendigkeit internationaler Zusammenarbeit,\nbetonend die Bedeutung des Multilateralismus,\nmit Sorge zur Kenntnis nehmend die aktuelle Situation,\n\nZu formatierender Text:", + "resolutionImportLLMTitle": "KI-Formatierung", + "resolutionImportOperative": "Operative Absätze importieren", + "resolutionImportPreamble": "Präambelabsätze importieren", + "resolutionImportPreview": "Vorschau: {count} Absätze erkannt", + "resolutionImportTipsOperative1": "Nummerierte Hauptabsätze: 1. 2. 3. oder 1) 2) 3)", + "resolutionImportTipsOperative2": "Unterabsätze mit Buchstaben: a) b) c) oder (a) (b) (c)", + "resolutionImportTipsOperative3": "Verschachtelte Unterabsätze mit römischen Ziffern: i) ii) iii)", + "resolutionImportTipsOperative4": "Weitere Verschachtelung mit Doppelbuchstaben: aa) bb) cc)", + "resolutionImportTipsPreamble1": "Jeder Absatz sollte mit einem Komma enden", + "resolutionImportTipsPreamble2": "Zeilenumbrüche trennen die einzelnen Absätze", + "resolutionImportTipsPreamble3": "Die Absätze werden in der eingegebenen Reihenfolge importiert", + "resolutionImportTipsTitle": "Tipps für optimale Ergebnisse", + "resolutionIndent": "Einrücken", + "resolutionMoveDown": "Nach unten", + "resolutionMoveUp": "Nach oben", + "resolutionNoClausesYet": "Noch keine Absätze vorhanden.", + "resolutionNoOperativeClauses": "Noch keine operativen Absätze vorhanden.", + "resolutionNoPreambleClauses": "Noch keine Präambelabsätze vorhanden.", + "resolutionOperativeClauses": "Operative Absätze", + "resolutionOperativePlaceholder": "Operativen Absatz eingeben...", + "resolutionOutdent": "Ausrücken", + "resolutionPaper": "Resolutionspapier", + "resolutionPapers": "Resolutionspapiere", + "resolutionPreambleClauses": "Präambelabsätze", + "resolutionPreamblePlaceholder": "Präambelabsatz eingeben...", + "resolutionPreview": "Vorschau", + "resolutionRejected": "Resolution abgelehnt", + "resolutionSentBack": "Resolution zurückverwiesen", + "resolutionShowPreview": "Vorschau anzeigen", + "resolutionSponsoringDelegations": "Unterstützerstaaten", + "resolutionSubClausePlaceholder": "Unterabsatz eingeben...", + "resolutionSubClauses": "Unterabsätze", + "resolutionUnknownPhrase": "Unbekannter Operator", + "resolutions": "Resolutionen", + "restoreContentFromSnapshot": "Inhalt von vor den Änderungsanträgen wiederherstellen", + "restoreContentFromSnapshotDescription": "Alle angewendeten Änderungsanträge rückgängig machen und den Resolutionsinhalt auf die Version vor der Änderungsantragsphase zurücksetzen. Angewendete Änderungsanträge werden auf 'Ausstehend' zurückgesetzt.", + "revertDrWarning": "Durch das Zurücksetzen wird die Dokumentennummer entfernt. Das Papier kann später erneut befördert werden.", + "revertStatus": "Status zurücksetzen", + "revertVotingWarning": "Durch das Zurücksetzen werden alle Absatzstimmungen für dieses Papier gelöscht.", "role": "Rolle", "rollCall": "Anwesenheitsfeststellung", "rollCallError": "Gremienmitglied nicht gefunden", @@ -194,36 +457,85 @@ "rollCollError": "Gremienmitglied nicht gefunden", "rollCollSuccess": "Anwesenheitsfeststellung abgeschlossen", "save": "Speichern", + "saveChanges": "Änderungen speichern", + "saveError": "Speichern fehlgeschlagen", + "savingChanges": "Speichern...", "searchCommitteeMembers": "Gremienmitglieder durchsuchen", + "searchMembers": "Mitglieder suchen...", + "searchUsers": "Benutzer suchen...", "selectAgendaItem": "Tagesordnungspunkt auswählen...", + "selectAmendmentType": "Antragstyp wählen", + "selectAuthorDelegation": "Autoren-Delegation auswählen", + "selectCommitteeMember": "Gremienmitglied auswählen...", + "selectConferenceMember": "Konferenzmitglied auswählen...", + "selectProposerDelegation": "Antragstellende Delegation auswählen", + "selectTargetClause": "Zielabsatz auswählen", "selected": "Ausgewählt", + "sendBack": "Zurückverweisen", + "sentBack": "Zurückverwiesen", "seoDescription": "MUNify CHASE ist das kostenlose Open-Source-Debattenmanagement-Tool für Model United Nations Konferenzen. Redelisten, Abstimmungen und Resolutionen digital verwalten.", "seoTitle": "MUNify CHASE – Debattenmanagement für Model United Nations", + "setActiveAmendment": "Behandeln", + "setActiveDr": "Aktiv setzen", + "setActiveDrHint": "Setze einen Resolutionsentwurf in der Vorsitzansicht als aktiv, um ihn hier anzuzeigen.", "setAllAbsent": "Alle Abwesend setzen", "setAllPresent": "Alle Anwesend setzen", "setStatus": "Status ändern", "setup": "Setup", "sha": "SHA", + "shareCode": "Freigabecode", + "shareCodes": "Freigabecodes", "short_sleek_snake_hint": "Gremium", "showOfHandsVoting": "Abstimmung per Handzeichen", "simpleMajority": "Einfach", + "simpleMajorityTooltip": "Benötigte Stimmen für die einfache Mehrheit", "speaker": "Redner*in", "speakersList": "Redeliste", "speakersListNamePlaceholder": "Neuer Name...", "speakersListNotFound": "Redeliste nicht gefunden", "speakersListOvertime": "Redezeit abgelaufen!", "spectator": "Zuschauer*in", + "sponsor": "Unterstützerstaaten", + "sponsorAdded": "Sponsor hinzugefügt", + "sponsorAmendment": "Unterstützen", + "sponsorCount": "{count} Unterstützerstaaten", + "sponsorPaper": "Unterstützen", + "sponsorRemoved": "Sponsor entfernt", + "sponsorThreshold": "{current}/{needed} Unterstützer ({percent}% benötigt)", + "sponsors": "Unterstützerstaaten", + "startAmendmentPhase": "Änderungsantragsphase starten", + "startEditing": "Bearbeitung starten", "startVote": "Abstimmung starten", + "startVotingPhase": "Abstimmungsphase starten", + "startVotingPhaseDescription": "Zur Abstimmungsphase wechseln, in der jeder operative Absatz einzeln abgestimmt wird.", "stateOfDebate": "Debattenstand", + "statusReverted": "Status zurückgesetzt", "statusUpdated": "Status wurde gesetzt", "strikethrough": "Durchgestrichen", "submit": "Absenden", + "submitAmendment": "Änderungsantrag einreichen", "submitImg": "Bild einfügen", + "submitPaper": "Papier einreichen", "submitStateOfDebate": "Debattenstatus speichern", "submitStatus": "Status setzen", + "submitToChair": "An Vorsitz einreichen", + "submitted": "Eingereicht", + "submittedBy": "Eingereicht durch", + "submittedPapers": "Eingereichte Papiere", + "submittedPapersDescription": "Von Delegierten eingereichte Papiere, sortiert nach Unterstützerzahl", + "submittingNation": "Einreichender Staat", + "supportDraftResolution": "Unterstützen", + "supportReEvaluation": "Änderung der Unterstützung", + "supportReEvaluationClosed": "Änderung der Unterstützung nicht erlaubt", + "supportReEvaluationNotOpen": "Änderung der Unterstützung ist derzeit nicht erlaubt", + "supportReEvaluationOpen": "Änderung der Unterstützung erlaubt", + "supporterCount": "{count} Unterstützerstaaten", "suspension": "Vertagung", + "targetPosition": "Zielposition", "teamMember": "Teammitglied", + "teamOnly": "Nur Team", "theme": "Theme", + "thresholdNotMet": "Unterstützerschwelle nicht erreicht", "timeOver": "Redezeit ist abgelaufen!", "timer": "Zeit", "toastAddError": "{ targetName } konnte nicht hinzugefügt werden", @@ -241,33 +553,61 @@ "toastUpdateError": "{targetName} konnte nicht aktualisiert werden", "toastUpdateLoading": "{targetName} wird aktualisiert...", "toastUpdateSuccess": "{targetName} aktualisiert", + "topCandidate": "Top-Kandidat", + "totalCountriesPresent": "Anzahl anwesender Staaten", "twoThirdsMajority": "Zwei-Drittel", + "twoThirdsMajorityTooltip": "Benötigte Stimmen für 2/3-Mehrheit", "typeOfVoting": "Abstimmungsart", "unActor": "UN-Akteur", "unActors": "UN-Akteure", + "unassigned": "Nicht zugewiesen", "underline": "Unterstrichen", "undo": "Rückgängig", + "undoVote": "Stimmung rückgängig", "unknown": "unbekannt", "unrecognizedCodes": "Nicht erkannte Codes:", "until": "bis {time} Uhr", + "untitledPaper": "Unbenanntes Papier", "updatedStateOfDebate": "Debattenstatus gespeichert", "updatingStateOfDebate": "Debattenstatus wird gespeichert...", "updatingStatus": "Status wird gesetzt...", "updatingWhiteboard": "Whiteboard veröffentlichen...", "upload": "Hochladen", "url": "URL", + "useFullVoting": "Volles Abstimmungssystem nutzen", "userAlreadyExists": "Benutzer existiert bereits in dieser Konferenz: {email}", + "users": "Benutzer", "version": "Version", + "viewPaper": "Papier ansehen", + "voteOnParagraph": "Abstimmung über OP {index}", + "voteOutcome": "Abstimmungsergebnis", + "voteResult": "Abstimmungsergebnis", "voteTitel": "Name der Abstimmung", "voteTitleDescription": "Der Titel der Abstimmung wird allen Teilnehmenden angezeigt und dient der Identifizierung. Wird es leer gelassen, wird als Fallback \"Abstimmung\" verwendet.", + "votesAbstain": "Enthaltungen", + "votesAgainst": "Gegenstimmen", + "votesFor": "Dafürstimmen", "voting": "Abstimmung", + "votingControlsPlaceholder": "Abstimmungssteuerung wird in einem zukünftigen Update verfügbar sein.", + "votingPhase": "Abstimmungsphase", + "votingPhaseActive": "Abstimmungsphase aktiv", + "votingPhaseStarted": "Abstimmungsphase gestartet", + "votingResults": "Abstimmungsergebnisse", + "waitingForAssignment": "Warte auf Zuweisung", + "waitingForAssignmentDescription": "Du wurdest noch keinem Gremium zugewiesen. Bitte warte, bis ein Admin dich zuweist.", "whiteboard": "Whiteboard", "whiteboardIsEmpty": "Das Whiteboard ist momentan leer...", "whiteboardPlaceholder": "Beginne hier zu schreiben...", "whiteboardUpdated": "Whiteboard veröffentlicht", "withAbstentions": "Enthaltungen", + "withdrawAmendment": "Zurückziehen", + "withdrawSponsorship": "Unterstützung zurückziehen", + "withdrawSupport": "Unterstützung zurückziehen", "withoutAbstentions": "Keine Enthaltungen", + "workingPaper": "Arbeitspapier", + "workingPapers": "Arbeitspapiere", "yes": "Ja", "you": "Du", - "youCannotEditYourself": "Du kannst deine eigene Rolle nicht bearbeiten" + "youCannotEditYourself": "Du kannst deine eigene Rolle nicht bearbeiten", + "youreUp": "Du bist dran!" } diff --git a/messages/en.json b/messages/en.json index f37f213a..d4c7011a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -5,22 +5,74 @@ "absent": "Absent", "absoluteMajority": "Absolute", "abstain": "Abstain", + "activeAmendment": "Being Discussed", + "activeDraftResolution": "In Progress", "addAgendaItem": "Add Item", "addAll": "Add All", + "addClause": "Add Clause", + "addClausePresentation": "Add Clause", + "addComment": "Add Comment", "addCommittee": "Add Committee", "addCountriesCount": "Add {count} countries", "addCountry": "Add Country", + "addMeToList": "Add me to list", "addMember": "Add Member", "addNonStateActor": "Add NGO", + "addRepresentation": "Add Delegation", + "addSponsor": "Add Sponsor", "addUnActor": "Add UN Actor", "admin": "Admin", + "adoptByConsensus": "Adopt by Consensus", + "adoptClause": "Adopt", + "adoptResolution": "Adopt Resolution", + "adopted": "Adopted", "adoptionAnnouncement": "BREAKING: Resolution on \"{agendaItem}\" adopted in the committee {committeeName}", + "advanceToNextParagraph": "Advance to Next Paragraph", "agendaItem": "Agenda item", "agendaItemTitle": "Agenda Item Title", "agendaItems": "Agenda Items", "allRightsReservedby": "All rights reserved by", + "allowSelfAddToSpeakersList": "Self-add to Speakers List", + "allowSelfAddToSpeakersListDescription": "Allow delegates and non-state actors to add themselves to speakers lists.", + "alterClausePresentation": "Alter Text", + "alterPosition": "Move Clause", + "alterText": "Alter Text", + "amendment": "Amendment", + "amendmentAccepted": "Accepted", + "amendmentAcceptedToast": "Amendment accepted", + "amendmentAdd": "Add clause", + "amendmentAdopted": "Amendment adopted", + "amendmentAlterPosition": "Alter position", + "amendmentAlterText": "Alter text", + "amendmentConsensusAdopted": "Adopted by Consensus", + "amendmentCreated": "Amendment proposed", + "amendmentDelete": "Delete clause", + "amendmentPending": "Pending", + "amendmentPhase": "Amendment Phase", + "amendmentPhaseActive": "Amendment phase active", + "amendmentPhaseStarted": "Amendment phase started", + "amendmentProposed": "Amendment proposed", + "amendmentQueue": "Amendment Queue", + "amendmentRejected": "Rejected", + "amendmentRejectedClause": "Rejected", + "amendmentRejectedToast": "Amendment rejected", + "amendmentSponsoring": "Amendment Sponsoring", + "amendmentSponsoringClosed": "Amendment sponsoring is closed", + "amendmentSponsoringOpen": "Delegates can sponsor amendments", + "amendmentSubmission": "Amendment Submission", + "amendmentSubmissionClosed": "Amendment submission is closed", + "amendmentSubmissionOpen": "Delegates can submit new amendments", + "amendmentSubmitted": "Submitted", + "amendmentSubmittedToast": "Amendment submitted", + "amendmentUpdated": "Amendment updated", + "amendmentWithdrawn": "Withdrawn", + "amendmentWithdrawnToast": "Amendment withdrawn", + "amendments": "Amendments", "announceAdoption": "Announce Adoption", + "assignedCount": "{count} assigned", + "assignment": "Assignment", "back": "Back", + "backToResolutions": "Back to Resolutions", "baseFontSize": "Base Font Size", "baseFontSizeDescription": "Here you can set the base font size for the presentation view.", "blockquote": "Quote", @@ -28,18 +80,39 @@ "bulkAddMembers": "Bulk Add Members", "bulkEmailPlaceholder": "Enter email addresses (one per line or comma-separated)", "bulletedList": "Bulleted list", + "cancel": "Cancel", "chairControls": "Chair Controls", + "chairCreateAmendment": "Create Amendment", + "chairCreateWorkingPaper": "Create Working Paper", "changeSpeakersName": "Change Name", "changeSpeakersTime": "Change Speaking Time", + "changesSaved": "Saved", + "clauseComments": "on clauses", + "clauseLockedBy": "Being edited by {country}", + "clauseVoteDeleted": "Clause vote removed", + "clauseVoteRecorded": "Clause vote recorded", + "clauseVoteSummary": "Clause Vote Summary", + "clausesVoted": "{voted}/{total} clauses voted", + "clearActiveDr": "Clear Active", "clearFormatting": "Delete formatting", "clearList": "Reset List", "clearListDescription": "Are you sure you want to reset the entire list?", "close": "Close", "closeList": "Close List", + "closeReEvaluation": "Close Re-evaluation", "code": "Code", + "codeCopied": "Code copied!", + "codeRedeemed": "Code redeemed successfully", "codesUnrecognized": "{count} codes unrecognized", + "collaborativeEditingInfo": "Other delegates are editing this resolution. Hover a clause and click \"Start editing\" to begin. Locks expire automatically after 1 minute of inactivity.", "comingSoon": "coming soon", + "commentDeleted": "Comment deleted", "commentList": "Point of Information", + "commentPlaceholder": "Write a comment...", + "commentPosted": "Comment posted", + "commentUpdated": "Comment updated", + "comments": "Comments", + "commentsOnClause": "{count} comment(s)", "committee": "Committee", "committeeAbbreviation": "Committee Abbreviation", "committeeDoesNotExist": "The committee does not exist.", @@ -50,6 +123,7 @@ "committeeOverview": "Committee Overview", "committeeStatus": "Committee Status", "committeeStatusExpired": "{status} expired!", + "committees": "Committees", "con": "Against", "conferenceCreated": "Conference created!", "conferenceCreationError": "Could not create conference", @@ -58,7 +132,24 @@ "conferenceMembers": "Conference Members", "conferenceTitle": "Conference Title", "configuration": "Configuration", + "confirmAdoptByConsensus": "Adopt this amendment by consensus? This will immediately apply the change.", + "confirmAdoptResolution": "Adopt this resolution? Confetti will celebrate the adoption!", + "confirmDeleteAmendment": "Are you sure you want to propose deleting this clause?", + "confirmDeleteCommittee": "Are you sure you want to delete this committee? All associated data will be lost.", + "confirmDeletePaper": "Are you sure you want to delete this working paper? It will be hidden for all participants.", + "confirmDeleteRepresentation": "Are you sure you want to remove this delegation? Associated committee memberships will be removed.", + "confirmFinalVote": "Record this final vote? The resolution status will change to Final.", + "confirmRejectAmendment": "Reject this amendment?", + "confirmRejectResolution": "Reject this resolution?", "confirmRemoveMember": "Are you sure you want to remove this member?", + "confirmRevertStatus": "Revert this paper from {from} back to {to}?", + "confirmSendBack": "Send this resolution back?", + "confirmStartAmendmentPhase": "Start the amendment phase for this draft resolution? Delegates will be able to propose amendments paragraph by paragraph.", + "confirmStartVotingPhase": "Start the voting phase for this draft resolution? Each operative paragraph will be voted on individually.", + "confirmSubmitPaper": "Are you sure you want to submit this paper to the chair? You will no longer be able to edit it.", + "copy": "Copy", + "copyCode": "Copy", + "copyFailed": "Copy failed", "countries": "Countries", "countriesRecognized": "{count} countries recognized", "countryCodesHelp": "Supports Alpha-2 (DE, US) and Alpha-3 (DEU, USA) codes. Separate with spaces, commas, semicolons, or new lines.", @@ -67,28 +158,66 @@ "countryNsaOrCustomRole": "Country, NSA, or special role", "create": "Create", "createConference": "Create Conference", + "createPaper": "Create Paper", + "createResolutionPaper": "Create Working Paper", + "createShareCodeEdit": "Create Edit Code", + "createShareCodeSponsor": "Create Sponsor Code", + "currentParagraph": "Current Paragraph", + "currentText": "Current Text", "customName": "Custom name...", "dateCannotBeInPast": "The date must not be in the past!", + "debateControls": "Debate Controls", "delegate": "Delegate", "delegations": "Delegations", + "deleteClause": "Delete Clause", + "deleteClausePresentation": "Delete Clause", + "deleteCode": "Delete Code", + "deleteComment": "Delete", + "deleteConference": "Delete Conference", + "deleteConferenceConfirmation": "Type the conference name to confirm deletion:", + "deleteConferenceWarning": "This action is irreversible. All data associated with this conference will be permanently deleted.", + "deletePaper": "Delete Paper", + "deleteRepresentation": "Remove Delegation", "displayRegionalGroups": "Display Regional Blocs", + "documentLevelComments": "Document Comments", + "documentNumber": "Document Number", + "documentWide": "document-wide", + "doneEditing": "Done editing", "download": "Download", "downloadPresenceData": "Presence Data", + "draftResolution": "Draft Resolution", + "draftResolutions": "Draft Resolutions", "edit": "Edit", + "editAccess": "Edit Access", + "editAmendment": "Edit Amendment", + "editComment": "Edit", + "editPaper": "Edit Paper", + "editUser": "Edit User", + "editors": "Editors", "email": "Email", "enterAlpha2Code": "Please enter Alpha2Code", + "enterCode": "Enter Code", "enterCountryCodes": "Enter country codes (Alpha-2 or Alpha-3)", "errorUpdatingStateOfDebate": "Error saving debate status", "errorUpdatingStatus": "Status could not be set", "errorUpdatingTimer": "Could not update speaking time", "errorUpdatingWhiteboard": "Publication failed", "fileParseError": "Error parsing file", + "finalResolution": "Final Resolution", + "finalVote": "Final Vote", + "finalVoteDescription": "Record the final vote on the entire resolution.", + "finishAmendmentPhaseFirst": "Finish the amendment phase first", "formalDebate": "Formal debate", "forward": "Next", + "general": "General", + "goToAmendments": "Go to amendments", + "goToVoting": "Go to voting", "gotoSettings": "Go to settings", "h1": "Heading 1", "h2": "Heading 2", "h3": "Heading 3", + "hasModeratedCaucus": "Moderated Caucus", + "hasModeratedCaucusDescription": "Enable moderated informal caucus as a committee status option.", "home": "Home", "homeAboutText": "CHASE (CHAirSoftwarE) is a web application for managing and conducting debates at Model United Nations conferences. It is designed for both chairs and delegates. CHASE allows chairs to easily manage debates, while delegates can follow the discussion and collaborate with others in an intuitive and structured way. CHASE is free and open-source software.", "homeAboutTitle": "About CHASE", @@ -116,34 +245,38 @@ "importFromDelegator": "Import conference from Delegator", "imprintAndPrivacy": "Imprint & Privacy Policy", "informalCaucus": "Informal meeting", + "insertAfterPresentation": "Insert after OP.{index}", + "insertAsFirstClause": "Insert as first clause", + "insertAtBeginning": "Insert at beginning", + "invalidShareCode": "Invalid share code", "italic": "Italics", + "language": "Language", "launcher": "Launcher", "launcherDescription": "Select the conference", "launcherNoConferences": "You are not registered for a conference.", "launcherWelcome": "Welcome back, {name}!", "layout": "Layout", "layoutDescription": "Layout templates for the presentation view. Please note that changing the layout template here will overwrite manual layout changes.", - "layoutPreset": [ - { - "declarations": ["input preset"], - "match": { - "preset=*": "Unknown Layout", - "preset=default": "Default Layout", - "preset=smallScreen": "Layout for Small Screens" - }, - "selectors": ["preset"] - } - ], + "layoutPresetDefault": "Default Layout", + "layoutPresetResolution": "Resolution Layout", + "layoutPresetSmallScreen": "Layout for Small Screens", "layoutSelect": "Select Layout", "link": "Hyperlink", "listClosed": "List closed", + "listClosedCannotAdd": "The list is closed", "listEmpty": "No speech", + "lockAcquireFailed": "This clause is currently being edited by {country}. Please try again shortly.", "login": "Register", "logout": "Log out", "loose_slow_reindeer_build": "Committee Members", + "majorities": "Majorities", "majoritySettings": "Majority Settings", "majoritySettingsDescriptions": "Majority settings help visualize whether a motion has passed.", + "manageConferences": "Manage Conferences", "maroon_bland_ray_renew": "Committee abbreviation", + "matching": "matching", + "maxDraftResolutions": "Max Draft Resolutions", + "maxDraftResolutionsReached": "Maximum number of draft resolutions reached", "member": "Member", "memberAdded": "Member added successfully", "memberRemoved": "Member removed successfully", @@ -152,32 +285,87 @@ "minutesFromNow": "Relative time: Jump X minutes into the future", "missionControl": "Mission Control", "moderatedInformalCaucus": "Moderated informal caucus", + "moveClausePresentation": "Move Clause", + "moveToPositionPresentation": "Move to position {position}", + "myAmendments": "My Amendments", + "myPapers": "My Papers", + "name": "Name", + "nextParagraph": "Next", "nextSpeaker": "Next Speech", "nextSpeakerDescription": "Do you really want to call the next speech? All remaining Points of Information will be discarded.", + "noActiveAgendaItem": "No active agenda item. A paper cannot be created right now.", + "noActiveDr": "No active draft resolution", + "noActiveDrForVoting": "No active draft resolution for voting", + "noActiveDraftResolution": "No active draft resolution", "noAgendaItemSelected": "No agenda item active", "noAgendaItemSelectedDescription": "To work with speakers' lists, you must first select an agenda item.", + "noAmendments": "No amendments yet", + "noAssignmentNeeded": "No member assignment needed for this role.", "noCommentList": "No Point of Information List", + "noComments": "No comments yet", "noCurrentSpeaker": "No speech", "noData": "No data", + "noDraftResolution": "No draft resolution set as active.", + "noDraftResolutionsYet": "No draft resolutions yet.", "noMembers": "No members yet", + "noOperativeClauses": "No operative clauses", + "noPapersYet": "No papers yet. Create one or enter a share code.", "noResults": "No results", + "noSubmittedPapers": "No submitted papers yet.", "nonStateActor": "Non-state Actor", "nonStateActors": "Non-state Actors", "notAuthorized": "You are not authorized to access this page", "notPresent": "Not present", + "notPresentCannotAdd": "You must be marked as present to add yourself", "nothingChanged": "Nothing changed", "numberedList": "Numbered list", "off": "Off", "on": "On", + "onListPosition": "You are #{position} on the list", "openPresentation": "Open Presentation View", + "openReEvaluation": "Open Re-evaluation", + "operativeClause": "Operative Clause", + "operativeClausePresentation": "Operative Clause", + "outcome": "Outcome", + "over": "over", + "paperCreated": "Paper created", + "paperDeleted": "Paper deleted", + "paperPromoted": "Paper promoted to Draft Resolution", + "paperSubmitted": "Paper submitted to chair", + "paperSupportThresholdTooltip": "Supporting states required to submit an amendment", + "paperTitle": "Paper Title", + "papers": "Papers", + "paragraphVoting": "Paragraph Voting", "parsedCountries": "Countries to add:", + "participantView": "Participant View", "pause": "Pause", + "phraseCopied": "Phrase copied!", + "phraseLookup": "Phrases", + "phraseLookupDisclaimer": "These phrases are provided as guidance. Please verify correct usage in context.", + "phraseLookupNoResults": "No phrases found.", + "phraseLookupSearch": "Search phrases...", + "phraseLookupTitle": "Phrase Reference", + "preambleClause": "Preamble Clause", "presence": "Presence", "present": "Present", "presentationMode": "Presentation View", + "pressWebsite": "Press Website", + "preview": "Preview", + "previousParagraph": "Previous", + "printResolution": "Print", "pro": "In Favor", + "promote": "Promote", + "promoteToDraftResolution": "Promote to Draft Resolution", + "promoteToDraftResolutionConfirm": "Promote this paper to a Draft Resolution? This will assign it a document number.", + "proposeAmendment": "Propose Amendment", + "proposedAmendmentPresentation": "Proposed Amendment", + "proposedBy": "Proposed by {name}", + "proposedText": "Proposed Text", + "publicComment": "Public", "publish": "Publish", "publishChanges": "Publish changes", + "recordVoteFromVoting": "Record Vote", + "redeemShareCode": "Redeem Share Code", "redo": "Repeat", "regionalGroup_africa": "Africa", "regionalGroup_asiaPacific": "Asia-Pacific", @@ -185,7 +373,82 @@ "regionalGroup_latinAmericaCaribbean": "Latin America and the Caribbean", "regionalGroup_westernEuropeOthers": "Western Europe and Others", "regionalGroups": "Regional Groups", + "rejectClause": "Reject", + "rejectResolution": "Reject Resolution", + "rejected": "Rejected", + "removeFromList": "Remove from list", "removeMember": "Remove", + "removeSponsor": "Remove Sponsorship", + "replyToComment": "Reply", + "resolution": "Resolution", + "resolutionAddClause": "Add Clause", + "resolutionAddContinuation": "Continuation Text", + "resolutionAddFirstClause": "Add First Clause", + "resolutionAddNested": "Nested Clause", + "resolutionAddSibling": "Add Clause", + "resolutionAddSubClause": "Sub-clause", + "resolutionAdopted": "Resolution adopted!", + "resolutionAuthoringDelegation": "Authoring Delegation", + "resolutionCommittee": "Committee", + "resolutionContinuationPlaceholder": "Enter continuation text...", + "resolutionDeleteBlock": "Delete Block", + "resolutionDeleteClause": "Delete", + "resolutionDisclaimer": "This document was created as part of a {conferenceName} simulation and has no legal validity.", + "resolutionEditor": "Resolution Editor", + "resolutionFeatureEnabled": "Resolution Features", + "resolutionFeatureEnabledDescription": "Enable resolution editor, papers, and amendment features for this conference.", + "resolutionFontSize": "Resolution Font Size", + "resolutionFontSizeDescription": "Set the font size for the resolution text in the presentation view.", + "resolutionHeadline": "Resolution Headline (e.g. The Security Council)", + "resolutionHidePreview": "Hide Preview", + "resolutionImport": "Import", + "resolutionImportButton": "Import {count} clause(s)", + "resolutionImportHintOperative": "Paste numbered operative clauses. Sub-clauses will be detected automatically.", + "resolutionImportHintPreamble": "Paste preamble clauses, separated by comma and line break.", + "resolutionImportLLMCopied": "Copied!", + "resolutionImportLLMCopyPrompt": "Copy Prompt", + "resolutionImportLLMInstructions": "Copy the following prompt into an AI assistant to automatically format your text:", + "resolutionImportLLMPromptOperative": "Format the following text as UN resolution operative clauses. Use:\n- Numbering for main clauses: 1. 2. 3.\n- Letters for sub-clauses: a) b) c)\n- Roman numerals for further nesting: i) ii) iii)\n- Double letters for deepest level: aa) bb) cc)\n- Semicolon at the end of each clause, period at the end of the last\n\nExample format:\n1. Calls upon all Member States to take measures;\n a) to promote peace;\n b) to strengthen cooperation;\n i) at the bilateral level;\n ii) at the multilateral level;\n2. Requests the Secretary-General to submit a report.\n\nText to format:", + "resolutionImportLLMPromptPreamble": "Format the following text as UN resolution preamble clauses. Each clause should:\n- Begin with a lowercase letter (except proper nouns)\n- End with a comma\n- Be separated by a line break\n\nExample format:\nrecalling its resolution 70/1 of 25 September 2015,\nemphasizing the importance of multilateralism,\nnoting with concern the current situation,\n\nText to format:", + "resolutionImportLLMTitle": "AI Formatting", + "resolutionImportOperative": "Import Operative Clauses", + "resolutionImportPreamble": "Import Preamble Clauses", + "resolutionImportPreview": "Preview: {count} clause(s) detected", + "resolutionImportTipsOperative1": "Numbered main clauses: 1. 2. 3. or 1) 2) 3)", + "resolutionImportTipsOperative2": "Lettered sub-clauses: a) b) c) or (a) (b) (c)", + "resolutionImportTipsOperative3": "Nested sub-clauses with Roman numerals: i) ii) iii)", + "resolutionImportTipsOperative4": "Further nesting with double letters: aa) bb) cc)", + "resolutionImportTipsPreamble1": "Each clause should end with a comma", + "resolutionImportTipsPreamble2": "Line breaks separate individual clauses", + "resolutionImportTipsPreamble3": "Clauses are imported in the order entered", + "resolutionImportTipsTitle": "Tips for Best Results", + "resolutionIndent": "Indent", + "resolutionMoveDown": "Move Down", + "resolutionMoveUp": "Move Up", + "resolutionNoClausesYet": "No clauses yet.", + "resolutionNoOperativeClauses": "No operative clauses yet.", + "resolutionNoPreambleClauses": "No preamble clauses yet.", + "resolutionOperativeClauses": "Operative Clauses", + "resolutionOperativePlaceholder": "Enter operative clause...", + "resolutionOutdent": "Outdent", + "resolutionPaper": "Resolution Paper", + "resolutionPapers": "Resolution Papers", + "resolutionPreambleClauses": "Preamble Clauses", + "resolutionPreamblePlaceholder": "Enter preamble clause...", + "resolutionPreview": "Preview", + "resolutionRejected": "Resolution rejected", + "resolutionSentBack": "Resolution sent back", + "resolutionShowPreview": "Show Preview", + "resolutionSponsoringDelegations": "Sponsoring Delegations", + "resolutionSubClausePlaceholder": "Enter sub-clause...", + "resolutionSubClauses": "Sub-clauses", + "resolutionUnknownPhrase": "Unknown phrase", + "resolutions": "Resolutions", + "restoreContentFromSnapshot": "Restore content from before amendments", + "restoreContentFromSnapshotDescription": "Undo all applied amendments and restore the resolution content to the version before the amendment phase began. Applied amendments will be reset to pending.", + "revertDrWarning": "Reverting will clear the document number. The paper can be re-promoted later.", + "revertStatus": "Revert Status", + "revertVotingWarning": "Reverting will delete all clause vote results for this paper.", "role": "Role", "rollCall": "Roll Call", "rollCallError": "Committee member not found", @@ -194,36 +457,85 @@ "rollCollError": "Committee member not found", "rollCollSuccess": "Roll call complete", "save": "Save", + "saveChanges": "Save Changes", + "saveError": "Save failed", + "savingChanges": "Saving...", "searchCommitteeMembers": "Search committee members", + "searchMembers": "Search members...", + "searchUsers": "Search users...", "selectAgendaItem": "Select agenda item...", + "selectAmendmentType": "Select amendment type", + "selectAuthorDelegation": "Select Author Delegation", + "selectCommitteeMember": "Select committee member...", + "selectConferenceMember": "Select conference member...", + "selectProposerDelegation": "Select Proposing Delegation", + "selectTargetClause": "Select Target Clause", "selected": "Selected", + "sendBack": "Send Back", + "sentBack": "Sent Back", "seoDescription": "MUNify CHASE is the free, open-source debate management tool for Model United Nations conferences. Manage speakers lists, voting, and resolutions digitally.", "seoTitle": "MUNify CHASE – Debate Management for Model United Nations", + "setActiveAmendment": "Discuss", + "setActiveDr": "Set Active", + "setActiveDrHint": "Set a draft resolution as active in the chair view to display it here.", "setAllAbsent": "Set All Absent", "setAllPresent": "Set All Present", "setStatus": "Change status", "setup": "Set up", "sha": "SHA", + "shareCode": "Share Code", + "shareCodes": "Share Codes", "short_sleek_snake_hint": "Committee", "showOfHandsVoting": "Vote by Show of Hands", "simpleMajority": "Simple", + "simpleMajorityTooltip": "Needed notes for simple majority", "speaker": "Speaker", "speakersList": "General Speakers' List", "speakersListNamePlaceholder": "New name...", "speakersListNotFound": "Speakers' list not found", "speakersListOvertime": "Speaking time over!", "spectator": "Spectator", + "sponsor": "Sponsor", + "sponsorAdded": "Sponsor added", + "sponsorAmendment": "Sponsor", + "sponsorCount": "{count} sponsors", + "sponsorPaper": "Sponsor", + "sponsorRemoved": "Sponsor removed", + "sponsorThreshold": "{current}/{needed} sponsors ({percent}% needed)", + "sponsors": "Sponsors", + "startAmendmentPhase": "Start Amendment Phase", + "startEditing": "Start editing", "startVote": "Start Vote", + "startVotingPhase": "Start Voting Phase", + "startVotingPhaseDescription": "Move to the voting phase where each operative paragraph will be voted on individually.", "stateOfDebate": "State of Debate", + "statusReverted": "Status reverted", "statusUpdated": "Status has been set", "strikethrough": "Strikethrough", "submit": "Submit", + "submitAmendment": "Submit Amendment", "submitImg": "Insert image", + "submitPaper": "Submit Paper", "submitStateOfDebate": "Save debate status", "submitStatus": "Set status", + "submitToChair": "Submit to Chair", + "submitted": "Submitted", + "submittedBy": "Submitted by", + "submittedPapers": "Submitted Papers", + "submittedPapersDescription": "Papers submitted by delegates, ranked by sponsor count", + "submittingNation": "Submitting Nation", + "supportDraftResolution": "Support", + "supportReEvaluation": "Support Re-evaluation", + "supportReEvaluationClosed": "Re-evaluation is closed", + "supportReEvaluationNotOpen": "Support re-evaluation is not currently open", + "supportReEvaluationOpen": "Re-evaluation is open — delegates can now change their support", + "supporterCount": "{count} supporters", "suspension": "Suspension", + "targetPosition": "Target Position", "teamMember": "Team Member", + "teamOnly": "Team Only", "theme": "Theme", + "thresholdNotMet": "Sponsor threshold not met", "timeOver": "Speaking time is up!", "timer": "Timer", "toastAddError": "Could not add { targetName }", @@ -241,33 +553,61 @@ "toastUpdateError": "Could not update {targetName}", "toastUpdateLoading": "Updating {targetName}...", "toastUpdateSuccess": "{targetName} updated", + "topCandidate": "Top Candidate", + "totalCountriesPresent": "Count of Present Countries", "twoThirdsMajority": "Two-thirds", + "twoThirdsMajorityTooltip": "Needed votes for two-thrids majority", "typeOfVoting": "Type of Vote", "unActor": "UN Actor", "unActors": "UN Actors", + "unassigned": "Unassigned", "underline": "Underlined", "undo": "Undo", + "undoVote": "Undo Vote", "unknown": "unknown", "unrecognizedCodes": "Unrecognized codes:", "until": "until {time}", + "untitledPaper": "Untitled Paper", "updatedStateOfDebate": "Debate status saved", "updatingStateOfDebate": "Saving debate status...", "updatingStatus": "Status is being set...", "updatingWhiteboard": "Publish whiteboard...", "upload": "Upload", "url": "URL", + "useFullVoting": "Use Full Voting", "userAlreadyExists": "User already exists in this conference: {email}", + "users": "Users", "version": "Version", + "viewPaper": "View Paper", + "voteOnParagraph": "Vote on OP {index}", + "voteOutcome": "Vote Outcome", + "voteResult": "Vote Result", "voteTitel": "Vote Title", "voteTitleDescription": "The vote title will be visible to all participants and is used for identification. If left empty, \"Vote\" will be used as fallback.", + "votesAbstain": "Abstentions", + "votesAgainst": "Votes Against", + "votesFor": "Votes For", "voting": "Voting", + "votingControlsPlaceholder": "Voting controls will be available in a future update.", + "votingPhase": "Voting Phase", + "votingPhaseActive": "Voting phase active", + "votingPhaseStarted": "Voting phase started", + "votingResults": "Voting Results", + "waitingForAssignment": "Waiting for Assignment", + "waitingForAssignmentDescription": "You have not been assigned to a committee yet. Please wait for an admin to assign you.", "whiteboard": "Whiteboard", "whiteboardIsEmpty": "The whiteboard is currently empty...", "whiteboardPlaceholder": "Start writing here...", "whiteboardUpdated": "Whiteboard published", "withAbstentions": "With Abstentions", + "withdrawAmendment": "Withdraw", + "withdrawSponsorship": "Withdraw Sponsorship", + "withdrawSupport": "Withdraw Support", "withoutAbstentions": "No Abstentions", + "workingPaper": "Working Paper", + "workingPapers": "Working Papers", "yes": "Yes", "you": "You", - "youCannotEditYourself": "You cannot edit your own role" + "youCannotEditYourself": "You cannot edit your own role", + "youreUp": "You're up!" } diff --git a/messages/pt.json b/messages/pt.json new file mode 100644 index 00000000..0d226892 --- /dev/null +++ b/messages/pt.json @@ -0,0 +1,613 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "aServiceBy": "Um serviço de", + "abort": "Cancelar", + "absent": "Ausente", + "absoluteMajority": "Absoluta", + "abstain": "Abstenção", + "activeAmendment": "Em Discussão", + "activeDraftResolution": "Em Andamento", + "addAgendaItem": "Adicionar Item", + "addAll": "Adicionar Todos", + "addClause": "Adicionar Cláusula", + "addClausePresentation": "Adicionar Cláusula", + "addComment": "Adicionar Comentário", + "addCommittee": "Adicionar Comité", + "addCountriesCount": "Adicionar {count} países", + "addCountry": "Adicionar País", + "addMeToList": "Inscrever-me na lista", + "addMember": "Adicionar Membro", + "addNonStateActor": "Adicionar ONG", + "addRepresentation": "Adicionar Delegação", + "addSponsor": "Adicionar Patrocinador", + "addUnActor": "Adicionar Ator da ONU", + "admin": "Administrador", + "adoptByConsensus": "Adotar por Consenso", + "adoptClause": "Adotar", + "adoptResolution": "Adotar Resolução", + "adopted": "Adotada", + "adoptionAnnouncement": "ÚLTIMA HORA: Resolução sobre \"{agendaItem}\" adotada no comité {committeeName}", + "advanceToNextParagraph": "Avançar para o Próximo Parágrafo", + "agendaItem": "Item da Agenda", + "agendaItemTitle": "Título do Item da Agenda", + "agendaItems": "Itens da Agenda", + "allRightsReservedby": "Todos os direitos reservados por", + "allowSelfAddToSpeakersList": "Autoinscrição na Lista de Oradores", + "allowSelfAddToSpeakersListDescription": "Permitir que delegados e atores não estatais se inscrevam nas listas de oradores.", + "alterClausePresentation": "Alterar Texto", + "alterPosition": "Mover Cláusula", + "alterText": "Alterar Texto", + "amendment": "Emenda", + "amendmentAccepted": "Aceita", + "amendmentAcceptedToast": "Emenda aceita", + "amendmentAdd": "Adicionar cláusula", + "amendmentAdopted": "Emenda adotada", + "amendmentAlterPosition": "Alterar posição", + "amendmentAlterText": "Alterar texto", + "amendmentConsensusAdopted": "Adotada por Consenso", + "amendmentCreated": "Emenda proposta", + "amendmentDelete": "Eliminar cláusula", + "amendmentPending": "Pendente", + "amendmentPhase": "Fase de Emendas", + "amendmentPhaseActive": "Fase de emendas ativa", + "amendmentPhaseStarted": "Fase de emendas iniciada", + "amendmentProposed": "Emenda proposta", + "amendmentQueue": "Fila de Emendas", + "amendmentRejected": "Rejeitada", + "amendmentRejectedClause": "Rejeitada", + "amendmentRejectedToast": "Emenda rejeitada", + "amendmentSponsoring": "Patrocínio de Emendas", + "amendmentSponsoringClosed": "O patrocínio de emendas está encerrado", + "amendmentSponsoringOpen": "Delegados podem patrocinar emendas", + "amendmentSubmission": "Submissão de Emendas", + "amendmentSubmissionClosed": "A submissão de emendas está encerrada", + "amendmentSubmissionOpen": "Delegados podem submeter novas emendas", + "amendmentSubmitted": "Submetida", + "amendmentSubmittedToast": "Emenda submetida", + "amendmentUpdated": "Emenda atualizada", + "amendmentWithdrawn": "Retirada", + "amendmentWithdrawnToast": "Emenda retirada", + "amendments": "Emendas", + "announceAdoption": "Anunciar Adoção", + "assignedCount": "{count} atribuídos", + "assignment": "Atribuição", + "back": "Voltar", + "backToResolutions": "Voltar às Resoluções", + "baseFontSize": "Tamanho Base da Fonte", + "baseFontSizeDescription": "Aqui você pode definir o tamanho base da fonte para a vista de apresentação.", + "blockquote": "Citação", + "bold": "Negrito", + "bulkAddMembers": "Adicionar Membros em Massa", + "bulkEmailPlaceholder": "Insira os endereços de e-mail (um por linha ou separados por vírgulas)", + "bulletedList": "Lista com marcadores", + "cancel": "Cancelar", + "chairControls": "Controlos da Presidência", + "chairCreateAmendment": "Criar Emenda", + "chairCreateWorkingPaper": "Criar Documento de Trabalho", + "changeSpeakersName": "Alterar Nome", + "changeSpeakersTime": "Alterar Tempo de Fala", + "changesSaved": "Salvo", + "clauseComments": "nas cláusulas", + "clauseLockedBy": "Em edição por {country}", + "clauseVoteDeleted": "Votação de cláusula removida", + "clauseVoteRecorded": "Votação de cláusula registada", + "clauseVoteSummary": "Resumo da Votação de Cláusulas", + "clausesVoted": "{voted}/{total} cláusulas votadas", + "clearActiveDr": "Limpar Ativo", + "clearFormatting": "Limpar formatação", + "clearList": "Limpar Lista", + "clearListDescription": "Tem a certeza de que deseja limpar toda a lista?", + "close": "Fechar", + "closeList": "Fechar Lista", + "closeReEvaluation": "Fechar Reavaliação", + "code": "Código", + "codeCopied": "Código copiado!", + "codeRedeemed": "Código resgatado com sucesso", + "codesUnrecognized": "{count} códigos não reconhecidos", + "collaborativeEditingInfo": "Outros delegados estão a editar esta resolução. Passe o rato sobre uma cláusula e clique em \"Começar a editar\" para começar. Os bloqueios expiram automaticamente após 1 minuto de inatividade.", + "comingSoon": "em breve", + "commentDeleted": "Comentário eliminado", + "commentList": "Ponto de Informação", + "commentPlaceholder": "Escreva um comentário...", + "commentPosted": "Comentário publicado", + "commentUpdated": "Comentário atualizado", + "comments": "Comentários", + "commentsOnClause": "{count} comentário(s)", + "committee": "Comité", + "committeeAbbreviation": "Abreviatura do Comité", + "committeeDoesNotExist": "O comité não existe.", + "committeeId": "ID do Comité", + "committeeMember": "Membro do Comité", + "committeeMembers": "Membros do Comité", + "committeeName": "Nome do Comité", + "committeeOverview": "Visão Geral do Comité", + "committeeStatus": "Status do Comité", + "committeeStatusExpired": "{status} expirou!", + "committees": "Comités", + "con": "Contra", + "conferenceCreated": "Conferência criada!", + "conferenceCreationError": "Não foi possível criar a conferência", + "conferenceCreationSuccessful": "Conferência criada. Será redirecionado de seguida...", + "conferenceId": "ID da Conferência", + "conferenceMembers": "Membros da Conferência", + "conferenceTitle": "Título da Conferência", + "configuration": "Configuração", + "confirmAdoptByConsensus": "Adotar esta emenda por consenso? Isto aplicará a alteração imediatamente.", + "confirmAdoptResolution": "Adotar esta resolução? Confetes celebrarão a adoção!", + "confirmDeleteAmendment": "Tem a certeza de que deseja propor a eliminação desta cláusula?", + "confirmDeleteCommittee": "Tem a certeza de que deseja excluir este comité? Todos os dados associados serão perdidos.", + "confirmDeletePaper": "Tem a certeza de que deseja excluir este documento de trabalho? Ficará oculto para todos os participantes.", + "confirmDeleteRepresentation": "Tem a certeza de que deseja remover esta delegação? Os membros associados ao comité serão removidos.", + "confirmFinalVote": "Gravar esta votação final? O estado da resolução será alterado para Final.", + "confirmRejectAmendment": "Rejeitar esta emenda?", + "confirmRejectResolution": "Rejeitar esta resolução?", + "confirmRemoveMember": "Tem a certeza de que deseja remover este membro?", + "confirmRevertStatus": "Reverter este documento de {from} para {to}?", + "confirmSendBack": "Enviar esta resolução de volta?", + "confirmStartAmendmentPhase": "Iniciar a fase de emendas para este projeto de resolução? Os delegados poderão propor emendas parágrafo por parágrafo.", + "confirmStartVotingPhase": "Iniciar a fase de votação para este projeto de resolução? Cada parágrafo operativo será votado individualmente.", + "confirmSubmitPaper": "Tem a certeza de que deseja submeter este documento à presidência? Deixará de poder editá-lo.", + "copy": "Copiar", + "copyCode": "Copiar", + "copyFailed": "Falha ao copiar", + "countries": "Países", + "countriesRecognized": "{count} países reconhecidos", + "countryCodesHelp": "Suporta códigos Alpha-2 (DE, US) e Alpha-3 (DEU, USA). Separe com espaços, vírgulas, ponto e vírgula ou novas linhas.", + "countryCodesPlaceholder": "DE, USA, FRA\nem um por linha...", + "countryNotFound": "País não encontrado", + "countryNsaOrCustomRole": "País, ator não estatal ou função especial", + "create": "Criar", + "createConference": "Criar Conferência", + "createPaper": "Criar Documento", + "createResolutionPaper": "Criar Documento de Trabalho", + "createShareCodeEdit": "Criar Código de Edição", + "createShareCodeSponsor": "Criar Código de Patrocinador", + "currentParagraph": "Parágrafo Atual", + "currentText": "Texto Atual", + "customName": "Nome personalizado...", + "dateCannotBeInPast": "A data não pode ser no passado!", + "debateControls": "Controlos do Debate", + "delegate": "Delegado", + "delegations": "Delegações", + "deleteClause": "Eliminar Cláusula", + "deleteClausePresentation": "Eliminar Cláusula", + "deleteCode": "Eliminar Código", + "deleteComment": "Eliminar", + "deleteConference": "Eliminar Conferência", + "deleteConferenceConfirmation": "Digite o nome da conferência para confirmar a eliminação:", + "deleteConferenceWarning": "Esta ação é irreversível. Todos os dados associados a esta conferência serão excluídos permanentemente.", + "deletePaper": "Excluir Documento", + "deleteRepresentation": "Remover Delegação", + "displayRegionalGroups": "Mostrar Grupos Regionais", + "documentLevelComments": "Comentários do Documento", + "documentNumber": "Número do Documento", + "documentWide": "todo o documento", + "doneEditing": "Terminar edição", + "download": "Descarregar", + "downloadPresenceData": "Dados de Presença", + "draftResolution": "Projeto de Resolução", + "draftResolutions": "Projetos de Resolução", + "edit": "Editar", + "editAccess": "Editar Acesso", + "editAmendment": "Editar Emenda", + "editComment": "Editar", + "editPaper": "Editar Documento", + "editUser": "Editar Usuário", + "editors": "Editores", + "email": "E-mail", + "enterAlpha2Code": "Por favor, insira o código Alpha2", + "enterCode": "Inserir Código", + "enterCountryCodes": "Insira os códigos dos países (Alpha-2 ou Alpha-3)", + "errorUpdatingStateOfDebate": "Erro ao gravar o estado do debate", + "errorUpdatingStatus": "Não foi possível definir o estado", + "errorUpdatingTimer": "Não foi possível atualizar o tempo de fala", + "errorUpdatingWhiteboard": "Falha na publicação", + "fileParseError": "Erro ao analisar o arquivo", + "finalResolution": "Resolução Final", + "finalVote": "Votação Final", + "finalVoteDescription": "Registar a votação final sobre a resolução inteira.", + "finishAmendmentPhaseFirst": "Finalize a fase de emendas primeiro", + "formalDebate": "Debate formal", + "forward": "Próximo", + "general": "Geral", + "goToAmendments": "Ir para emendas", + "goToVoting": "Ir para votação", + "gotoSettings": "Ir para as configurações", + "h1": "Título 1", + "h2": "Título 2", + "h3": "Título 3", + "hasModeratedCaucus": "Debate Moderado", + "hasModeratedCaucusDescription": "Ativar o debate informal moderado como opção de estado do comité.", + "home": "Início", + "homeAboutText": "CHASE (CHAirSoftwarE) é uma aplicação web para gerir e conduzir debates em conferências de Modelo das Nações Unidas. Foi projetada tanto para a presidência como para delegados. O CHASE permite que a presidência gira debates facilmente, enquanto os delegados podem acompanhar o debate e colaborar com outros de forma intuitiva e estruturada. O CHASE é um software livre e de código aberto.", + "homeAboutTitle": "Sobre o CHASE", + "homeCaption": "no século XXI", + "homeContactButton": "Entre em Contato", + "homeContactText": "Está a organizar uma conferência de Modelo das Nações Unidas e tem interesse em usar o CHASE? Oferecemos suporte gratuito (dentro das nossas possibilidades) para ajudá-lo a implantar o CHASE na sua própria infraestrutura. Também podemos hospedar o CHASE nos nossos servidores para a sua conferência. Entre em contato — teremos prazer em ajudar!", + "homeContactTitle": "Obtenha o CHASE para a sua Conferência", + "homeContributeButtonLabel": "MUNify no GitHub", + "homeContributeText": "O CHASE faz parte da iniciativa de código aberto 'MUNify' da DMUN. Isso significa que qualquer pessoa pode contribuir para o desenvolvimento. Agradecemos toda a ajuda que pudermos receber. Se tem experiência em desenvolvimento web ou quer aprender novas habilidades e ajudar, passe pelo nosso GitHub!", + "homeContributeTitle": "Contribua", + "homeHeroCardResolutionEditorText": "Crie e edite resoluções de forma conjunta com outros delegados. Acabou-se o papel ou o Google Docs!", + "homeHeroCardResolutionEditorTitle": "Resoluções", + "homeHeroCardSpeakersListText": "Gira as listas de oradores de forma simples e eficiente. Não há mais listas em papel!", + "homeHeroCardSpeakersListTitle": "Debates", + "homeHeroCardVotingText": "Gira moções e votações digitalmente com moções pré-configuradas baseadas nas suas Regras de Procedimento.", + "homeHeroCardVotingTitle": "Votação", + "homeHeroText": "A gestão de debates em conferências de Modelo das Nações Unidas finalmente recebe uma atualização.", + "homeMissionButtonLabel": "Saiba mais sobre a DMUN", + "homeMissionText": "O CHASE é desenvolvido por membros da organização alemã DMUN e.V. O nosso objetivo é oferecer uma alternativa gratuita e acessível a outras ferramentas de gestão de debates, facilitando a participação até em conferências menores. O CHASE foi inicialmente desenvolvido para conferências de língua alemã da DMUN — MUN-SH, MUNBW e MUNBB — mas estamos abertos a adaptá-lo para outras conferências.", + "homeMissionTitle": "A Nossa Missão", + "homeVersionButton": "CHASE (CHAirSoftwarE) é uma aplicação web para gerir e conduzir debates em conferências de Modelo das Nações Unidas. Foi projetada tanto para a presidência como para delegados. O CHASE permite que a presidência gira debates facilmente, enquanto os delegados podem acompanhar o debate e colaborar com outros delegados de forma intuitiva e estruturada. O CHASE é um software livre e de código aberto.", + "horizontalRule": "Divisor", + "icon": "Ícone", + "img": "Imagem", + "importFromDelegator": "Importar conferência a partir do Delegator", + "imprintAndPrivacy": "Imprensa & Política de Privacidade", + "informalCaucus": "Reunião informal", + "insertAfterPresentation": "Inserir após PO.{index}", + "insertAsFirstClause": "Inserir como cláusula primeira", + "insertAtBeginning": "Inserir no início", + "invalidShareCode": "Código de partilha inválido", + "italic": "Itálico", + "language": "Idioma", + "launcher": "Iniciador", + "launcherDescription": "Selecione a conferência", + "launcherNoConferences": "Não se encontra registado em nenhuma conferência.", + "launcherWelcome": "Bem-vindo de volta, {name}!", + "layout": "Esquema", + "layoutDescription": "Esquemas para a vista de apresentação. Tenha em conta que ao alterar o esquema de apresentação aqui substituirá as alterações manuais de apresentação.", + "layoutPresetDefault": "Esquema de apresentação Padrão", + "layoutPresetResolution": "Esquema de apresentação de Resolução", + "layoutPresetSmallScreen": "Esquema de apresentação para Telas Pequenas", + "layoutSelect": "Selecionar Esquema de apresentação", + "link": "Hiperligação", + "listClosed": "Lista fechada", + "listClosedCannotAdd": "A lista está fechada", + "listEmpty": "Nenhum discurso", + "lockAcquireFailed": "Esta cláusula está a ser editada por {country}. Por favor, tente novamente dentro de instantes.", + "login": "Iniciar sessão", + "logout": "Sair", + "loose_slow_reindeer_build": "Membros do Comité", + "majorities": "Maiorias", + "majoritySettings": "Configurações de Maioria", + "majoritySettingsDescriptions": "As configurações de maioria ajudam a verificar se uma moção foi aprovada.", + "manageConferences": "Gerir Conferências", + "maroon_bland_ray_renew": "Abreviatura do comité", + "matching": "correspondente", + "maxDraftResolutions": "Máximo de Projetos de Resolução", + "maxDraftResolutionsReached": "Número máximo de projetos de resolução atingido", + "member": "Membro", + "memberAdded": "Membro adicionado com sucesso", + "memberRemoved": "Membro removido com sucesso", + "memberUpdated": "Membro atualizado com sucesso", + "minuteOfTheHour": "Tempo absoluto: Ir para o minuto correspondente desta ou da próxima hora", + "minutesFromNow": "Tempo relativo: Avançar X minutos para o futuro", + "missionControl": "Controlo de Missão", + "moderatedInformalCaucus": "Debate informal moderado", + "moveClausePresentation": "Mover Cláusula", + "moveToPositionPresentation": "Mover para a posição {position}", + "myAmendments": "As Minhas Emendas", + "myPapers": "Os Meus Documentos", + "name": "Nome", + "nextParagraph": "Próximo", + "nextSpeaker": "Próximo Discurso", + "nextSpeakerDescription": "Deseja mesmo avançar para o próximo discurso? Todos os Pontos de Informação restantes serão descartados.", + "noActiveAgendaItem": "Nenhum item da agenda em curso. Não é possível criar um documento neste momento.", + "noActiveDr": "Nenhum projeto de resolução em curso", + "noActiveDrForVoting": "Nenhum projeto de resolução em curso para votação", + "noActiveDraftResolution": "Nenhum projeto de resolução em curso", + "noAgendaItemSelected": "Nenhum item da agenda em curso", + "noAgendaItemSelectedDescription": "Para trabalhar com as listas de oradores, deve selecionar primeiro um item da agenda.", + "noAmendments": "Nenhuma emenda ainda", + "noAssignmentNeeded": "Nenhuma atribuição de membro necessária para esta função.", + "noCommentList": "Sem Lista de Pontos de Informação", + "noComments": "Nenhum comentário ainda", + "noCurrentSpeaker": "Nenhum discurso", + "noData": "Sem dados", + "noDraftResolution": "Nenhum projeto de resolução definido como em curso.", + "noDraftResolutionsYet": "Nenhum projeto de resolução ainda.", + "noMembers": "Nenhum membro ainda", + "noOperativeClauses": "Nenhuma cláusula operativa", + "noPapersYet": "Nenhum documento ainda. Crie um ou insira um código de partilha.", + "noResults": "Sem resultados", + "noSubmittedPapers": "Nenhum documento submetido ainda.", + "nonStateActor": "Ator Não Estatal", + "nonStateActors": "Atores Não Estatais", + "notAuthorized": "Não tem autorização para aceder a esta página", + "notPresent": "Em falta", + "notPresentCannotAdd": "Tem de estar marcado como presente para se inscrever", + "nothingChanged": "Nada alterado", + "numberedList": "Lista numerada", + "off": "Desligado", + "on": "Ligado", + "onListPosition": "Você é o {position}.º na lista", + "openPresentation": "Abrir Vista de Apresentação", + "openReEvaluation": "Abrir Reavaliação", + "operativeClause": "Cláusula Operativa", + "operativeClausePresentation": "Cláusula Operativa", + "outcome": "Resultado", + "over": "encerrado", + "paperCreated": "Documento criado", + "paperDeleted": "Documento apagado", + "paperPromoted": "Documento elevado a Projeto de Resolução", + "paperSubmitted": "Documento submetido à presidência", + "paperSupportThresholdTooltip": "Estados apoiantes necessários para submeter uma emenda", + "paperTitle": "Título do Documento", + "papers": "Documentos", + "paragraphVoting": "Votação por Parágrafo", + "parsedCountries": "Países a adicionar:", + "participantView": "Vista do Participante", + "pause": "Pausar", + "phraseCopied": "Frase copiada!", + "phraseLookup": "Frases", + "phraseLookupDisclaimer": "Estas frases são fornecidas como orientação. Por favor, verifique o uso correto no contexto.", + "phraseLookupNoResults": "Nenhuma frase encontrada.", + "phraseLookupSearch": "Pesquisar frases...", + "phraseLookupTitle": "Referência de Frases", + "preambleClause": "Cláusula Preambular", + "presence": "Presença", + "present": "Presente", + "presentationMode": "Vista de Apresentação", + "pressWebsite": "Site de Imprensa", + "preview": "Pré-visualização", + "previousParagraph": "Anterior", + "printResolution": "Imprimir", + "pro": "A Favor", + "promote": "Promover", + "promoteToDraftResolution": "Promover a Projeto de Resolução", + "promoteToDraftResolutionConfirm": "Promover este documento a Projeto de Resolução? Será atribuído um número de documento.", + "proposeAmendment": "Propor Emenda", + "proposedAmendmentPresentation": "Emenda Proposta", + "proposedBy": "Proposta por {name}", + "proposedText": "Texto Proposto", + "publicComment": "Público", + "publish": "Publicar", + "publishChanges": "Publicar alterações", + "recordVoteFromVoting": "Registar Voto", + "redeemShareCode": "Resgatar Código de Partilha", + "redo": "Refazer", + "regionalGroup_africa": "África", + "regionalGroup_asiaPacific": "Ásia-Pacífico", + "regionalGroup_easternEurope": "Europa Oriental", + "regionalGroup_latinAmericaCaribbean": "América Latina e Caraíbas", + "regionalGroup_westernEuropeOthers": "Europa Ocidental e Outros", + "regionalGroups": "Grupos Regionais", + "rejectClause": "Rejeitar", + "rejectResolution": "Rejeitar Resolução", + "rejected": "Rejeitada", + "removeFromList": "Remover da lista", + "removeMember": "Remover", + "removeSponsor": "Remover Patrocínio", + "replyToComment": "Responder", + "resolution": "Resolução", + "resolutionAddClause": "Adicionar Cláusula", + "resolutionAddContinuation": "Texto de Continuação", + "resolutionAddFirstClause": "Adicionar Primeira Cláusula", + "resolutionAddNested": "Cláusula Aninhada", + "resolutionAddSibling": "Adicionar Cláusula", + "resolutionAddSubClause": "Subcláusula", + "resolutionAdopted": "Resolução adotada!", + "resolutionAuthoringDelegation": "Delegação Autora", + "resolutionCommittee": "Comité", + "resolutionContinuationPlaceholder": "Insira o texto de continuação...", + "resolutionDeleteBlock": "Excluir Grupo", + "resolutionDeleteClause": "Excluir", + "resolutionDisclaimer": "Este documento foi criado como parte de uma simulação da {conferenceName} e não possui validez jurídica.", + "resolutionEditor": "Editor de Resoluções", + "resolutionFeatureEnabled": "Funcionalidades de Resolução", + "resolutionFeatureEnabledDescription": "Ativar editor de resoluções, documentos de trabalho e funcionalidades de emendas para esta conferência.", + "resolutionFontSize": "Tamanho da Fonte da Resolução", + "resolutionFontSizeDescription": "Defina o tamanho da fonte para o texto da resolução na vista de apresentação.", + "resolutionHeadline": "Cabeçalho da Resolução (ex.: O Conselho de Segurança)", + "resolutionHidePreview": "Ocultar Pré-visualização", + "resolutionImport": "Importar", + "resolutionImportButton": "Importar {count} cláusula(s)", + "resolutionImportHintOperative": "Cole cláusulas operativas numeradas. Subcláusulas serão detectadas automaticamente.", + "resolutionImportHintPreamble": "Cole cláusulas preambulares, separadas por vírgulas e quebras de linha.", + "resolutionImportLLMCopied": "Copiado!", + "resolutionImportLLMCopyPrompt": "Copiar Comando", + "resolutionImportLLMInstructions": "Copie o seguinte comando em um assistente de IA para formatar o seu texto automaticamente:", + "resolutionImportLLMPromptOperative": "Formate o seguinte texto como cláusulas operativas de resolução da ONU. Use:\n- Numeração para cláusulas principais: 1. 2. 3.\n- Letras para subcláusulas: a) b) c)\n- Algarismos romanos para mais aninhamento: i) ii) iii)\n- Letras duplas para o nível mais profundo: aa) bb) cc)\n- Ponto e vírgula no final de cada cláusula, ponto final na última\n\nFormato de exemplo:\n1. Chama à atenção todos os membros para tomar medidas;\n a) para promover a paz;\n b) para fortalecer a cooperação;\n i) a nível bilateral;\n ii) a nível multilateral;\n2. Pede ao Secretário-Geral para submeter um relatório.\n\nTexto a formatar:", + "resolutionImportLLMPromptPreamble": "Formate o seguinte texto como cláusulas preambulares de resolução da ONU. Cada cláusula deve:\n- Começar com letra minúscula (exceto nomes próprios)\n- Terminar com vírgula\n- Ser separada por quebra de linha\n\nFormato de exemplo:\ninvocando a sua resolução 70/1 de 25 de setembro de 2015,\nenfatizando a importância do multilateralismo,\nrealçando com preocupação a situação atual,\n\nTexto a formatar:", + "resolutionImportLLMTitle": "Formatação com IA", + "resolutionImportOperative": "Importar Cláusulas Operativas", + "resolutionImportPreamble": "Importar Cláusulas Preambulares", + "resolutionImportPreview": "Pré-visualização: {count} cláusula(s) detectada(s)", + "resolutionImportTipsOperative1": "Cláusulas principais numeradas: 1. 2. 3. ou 1) 2) 3)", + "resolutionImportTipsOperative2": "Subcláusulas com letras: a) b) c) ou (a) (b) (c)", + "resolutionImportTipsOperative3": "Subcláusulas aninhadas com algarismos romanos: i) ii) iii)", + "resolutionImportTipsOperative4": "Mais aninhamento com letras duplas: aa) bb) cc)", + "resolutionImportTipsPreamble1": "Cada cláusula deve terminar com uma vírgula", + "resolutionImportTipsPreamble2": "Quebras de linha separam cláusulas individuais", + "resolutionImportTipsPreamble3": "As cláusulas são importadas na ordem inserida", + "resolutionImportTipsTitle": "Dicas para Melhores Resultados", + "resolutionIndent": "Recuar", + "resolutionMoveDown": "Mover para Baixo", + "resolutionMoveUp": "Mover para Cima", + "resolutionNoClausesYet": "Nenhuma cláusula ainda.", + "resolutionNoOperativeClauses": "Nenhuma cláusula operativa ainda.", + "resolutionNoPreambleClauses": "Nenhuma cláusula preambular ainda.", + "resolutionOperativeClauses": "Cláusulas Operativas", + "resolutionOperativePlaceholder": "Insira a cláusula operativa...", + "resolutionOutdent": "Diminuir recuo", + "resolutionPaper": "Documento de Resolução", + "resolutionPapers": "Documentos de Resolução", + "resolutionPreambleClauses": "Cláusulas Preambulares", + "resolutionPreamblePlaceholder": "Insira a cláusula preambular...", + "resolutionPreview": "Pré-visualização", + "resolutionRejected": "Resolução rejeitada", + "resolutionSentBack": "Resolução devolvida", + "resolutionShowPreview": "Mostrar Pré-visualização", + "resolutionSponsoringDelegations": "Delegações Patrocinadoras", + "resolutionSubClausePlaceholder": "Insira a subcláusula...", + "resolutionSubClauses": "Subcláusulas", + "resolutionUnknownPhrase": "Frase desconhecida", + "resolutions": "Resoluções", + "restoreContentFromSnapshot": "Restaurar conteúdo anterior às emendas", + "restoreContentFromSnapshotDescription": "Desfazer todas as emendas aplicadas e restaurar o conteúdo da resolução para a versão anterior ao início da fase de emendas. As emendas aplicadas serão redefinidas para pendentes.", + "revertDrWarning": "Reverter irá apagar o número do documento. O documento pode ser promovido novamente depois.", + "revertStatus": "Reverter Estado", + "revertVotingWarning": "Reverter irá excluir todos os resultados de votação de cláusulas deste documento.", + "role": "Função", + "rollCall": "Chamada", + "rollCallError": "Membro do comité não encontrado", + "rollCallSuccess": "Chamada concluída", + "rollCallVoting": "Votação Nominal", + "rollCollError": "Membro do comité não encontrado", + "rollCollSuccess": "Chamada concluída", + "save": "Guardar", + "saveChanges": "Guardar Alterações", + "saveError": "Falha ao guardar", + "savingChanges": "A guardar...", + "searchCommitteeMembers": "Pesquisar membros do comité", + "searchMembers": "Pesquisar membros...", + "searchUsers": "Pesquisar utilizadores...", + "selectAgendaItem": "Selecionar item da pauta...", + "selectAmendmentType": "Selecionar tipo de emenda", + "selectAuthorDelegation": "Selecionar Delegação Autora", + "selectCommitteeMember": "Selecionar membro do comité...", + "selectConferenceMember": "Selecionar membro da conferência...", + "selectProposerDelegation": "Selecionar Delegação Proponente", + "selectTargetClause": "Selecionar Cláusula Alvo", + "selected": "Selecionado", + "sendBack": "Devolver", + "sentBack": "Devolvida", + "seoDescription": "MUNify CHASE é a ferramenta gratuita e de código aberto para gestão de debates em conferências de Modelo das Nações Unidas. Gira listas de oradores, votações e resoluções digitalmente.", + "seoTitle": "MUNify CHASE – Gestão de Debates para Modelos das Nações Unidas", + "setActiveAmendment": "Debater", + "setActiveDr": "Definir como Ativo", + "setActiveDrHint": "Defina um projeto de resolução como ativo na vista da presidência para exibi-lo aqui.", + "setAllAbsent": "Marcar Todos como Ausentes", + "setAllPresent": "Marcar Todos como Presentes", + "setStatus": "Alterar estado", + "setup": "Configurar", + "sha": "SHA", + "shareCode": "Código de Partilha", + "shareCodes": "Códigos de Partilha", + "short_sleek_snake_hint": "Comité", + "showOfHandsVoting": "Votação por Levantamento de Mãos", + "simpleMajority": "Simples", + "simpleMajorityTooltip": "Votos necessários para maioria simples", + "speaker": "Orador", + "speakersList": "Lista Geral de Oradores", + "speakersListNamePlaceholder": "Novo nome...", + "speakersListNotFound": "Lista de oradores não encontrada", + "speakersListOvertime": "Tempo de fala esgotado!", + "spectator": "Espectador", + "sponsor": "Patrocinador", + "sponsorAdded": "Patrocinador adicionado", + "sponsorAmendment": "Patrocinar", + "sponsorCount": "{count} patrocinadores", + "sponsorPaper": "Patrocinar", + "sponsorRemoved": "Patrocinador removido", + "sponsorThreshold": "{current}/{needed} patrocinadores ({percent}% necessários)", + "sponsors": "Patrocinadores", + "startAmendmentPhase": "Iniciar Fase de Emendas", + "startEditing": "Começar a editar", + "startVote": "Iniciar Votação", + "startVotingPhase": "Iniciar Fase de Votação", + "startVotingPhaseDescription": "Passar para a fase de votação, onde cada parágrafo operativo será votado individualmente.", + "stateOfDebate": "Estado do Debate", + "statusReverted": "Estado revertido", + "statusUpdated": "Estado definido", + "strikethrough": "Riscado", + "submit": "Submeter", + "submitAmendment": "Submeter Emenda", + "submitImg": "Inserir imagem", + "submitPaper": "Submeter Documento", + "submitStateOfDebate": "Salvar estado do debate", + "submitStatus": "Definir estado", + "submitToChair": "Submeter à Presidência", + "submitted": "Submetido", + "submittedBy": "Submetido por", + "submittedPapers": "Documentos Submetidos", + "submittedPapersDescription": "Documentos submetidos por delegados, ordenados por número de patrocinadores", + "submittingNation": "Nação Proponente", + "supportDraftResolution": "Apoiar", + "supportReEvaluation": "Reavaliação de Apoio", + "supportReEvaluationClosed": "A reavaliação está encerrada", + "supportReEvaluationNotOpen": "A reavaliação de apoio não está aberta no momento", + "supportReEvaluationOpen": "A reavaliação está aberta — delegados podem alterar o seu apoio agora", + "supporterCount": "{count} apoiantes", + "suspension": "Suspensão", + "targetPosition": "Posição Alvo", + "teamMember": "Membro da Equipa", + "teamOnly": "Apenas Equipa", + "theme": "Tema", + "thresholdNotMet": "Limite de patrocinadores não atingido", + "timeOver": "Tempo de fala esgotado!", + "timer": "Cronómetro", + "toastAddError": "Não foi possível adicionar { targetName }", + "toastAddLoading": "A adicionar { targetName }...", + "toastAddSuccess": "{ targetName } adicionado(a)", + "toastCreateError": "Não foi possível criar {targetName}", + "toastCreateLoading": "A criar {targetName}...", + "toastCreateSuccess": "{targetName} criado(a)", + "toastDeleteError": "Não foi possível eliminar {targetName}", + "toastDeleteLoading": "A eliminar {targetName}...", + "toastDeleteSuccess": "{targetName} eliminado(a)", + "toastError": "Não foi possível carregar {targetName}", + "toastLoading": "A carregar {targetName}...", + "toastSuccess": "{targetName} carregado(a)", + "toastUpdateError": "Não foi possível atualizar {targetName}", + "toastUpdateLoading": "A atualizar {targetName}...", + "toastUpdateSuccess": "{targetName} atualizado(a)", + "topCandidate": "Principal Candidato", + "totalCountriesPresent": "Total de Países Presentes", + "twoThirdsMajority": "Dois terços", + "twoThirdsMajorityTooltip": "Votos necessários para maioria de dois terços", + "typeOfVoting": "Tipo de Votação", + "unActor": "Ator da ONU", + "unActors": "Atores da ONU", + "unassigned": "Não atribuído", + "underline": "Sublinhado", + "undo": "Desfazer", + "undoVote": "Desfazer Voto", + "unknown": "desconhecido", + "unrecognizedCodes": "Códigos não reconhecidos:", + "until": "até {time}", + "untitledPaper": "Documento Sem Título", + "updatedStateOfDebate": "Estado do debate gravado", + "updatingStateOfDebate": "A gravar estado do debate...", + "updatingStatus": "A definir estado...", + "updatingWhiteboard": "A publicar quadro branco...", + "upload": "Enviar", + "url": "URL", + "useFullVoting": "Usar Votação Completa", + "userAlreadyExists": "Utilizador já existe nesta conferência: {email}", + "users": "Utilizadores", + "version": "Versão", + "viewPaper": "Ver Documento", + "voteOnParagraph": "Votar no PO {index}", + "voteOutcome": "Resultado da Votação", + "voteResult": "Resultado da Votação", + "voteTitel": "Título da Votação", + "voteTitleDescription": "O título da votação será visível para todos os participantes e é usado para identificação. Se deixado em branco, \"Votação\" será usado como padrão.", + "votesAbstain": "Abstenções", + "votesAgainst": "Votos Contra", + "votesFor": "Votos a Favor", + "voting": "Votação", + "votingControlsPlaceholder": "Os controlos de votação estarão disponíveis numa atualização futura.", + "votingPhase": "Fase de Votação", + "votingPhaseActive": "Fase de votação ativa", + "votingPhaseStarted": "Fase de votação em curso", + "votingResults": "Resultados da Votação", + "waitingForAssignment": "A aguardar atribuição", + "waitingForAssignmentDescription": "Ainda não foi atribuído a um comité. Por favor, aguarde que um administrador o atribua.", + "whiteboard": "Quadro Branco", + "whiteboardIsEmpty": "O quadro branco está vazio no momento...", + "whiteboardPlaceholder": "Comece a escrever aqui...", + "whiteboardUpdated": "Quadro branco publicado", + "withAbstentions": "Com Abstenções", + "withdrawAmendment": "Retirar", + "withdrawSponsorship": "Retirar Patrocínio", + "withdrawSupport": "Retirar Apoio", + "withoutAbstentions": "Sem Abstenções", + "workingPaper": "Documento de Trabalho", + "workingPapers": "Documentos de Trabalho", + "yes": "Sim", + "you": "Você", + "youCannotEditYourself": "Não pode editar a sua própria função", + "youreUp": "É a sua vez!" +} diff --git a/package.json b/package.json index bcbcbd66..b467b485 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,13 @@ "@fontsource/roboto-mono": "^5.2.8", "@fontsource/vollkorn": "^5.2.10", "@friendofsvelte/tipex": "^0.0.8", + "@graphql-yoga/redis-event-target": "^3.0.3", "@inlang/cli": "^3.0.12", "@inlang/paraglide-js": "2.0.11", "@lingual/i18n-check": "^0.8.17", "@m1212e/graphql-scalars-houdini": "^0.0.1", "@m1212e/rumble": "^0.7.11", - "@m1212e/sveltekit-oidc": "^0.0.31", + "@m1212e/sveltekit-oidc": "0.0.39", "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/kit": "^2.49.4", "@sveltejs/vite-plugin-svelte": "^5.1.1", @@ -45,6 +46,7 @@ "hotkeys-js": "^3.13.15", "houdini": "^2.0.0-next.11", "houdini-svelte": "^3.0.0-next.13", + "ioredis": "^5.10.0", "jose": "^6.1.3", "js-yaml": "^4.1.1", "json-schema-to-typescript": "^15.0.4", @@ -103,6 +105,9 @@ }, "type": "module", "dependencies": { + "@deutschemodelunitednations/munify-resolution-editor": "v0.1.4", + "@tanstack/table-core": "^8.21.3", + "devalue": "^5.6.3", "drizzle-kit": "1.0.0-beta.1-fd5d1e8", "drizzle-orm": "1.0.0-beta.1-fd5d1e8", "pg": "^8.16.3" diff --git a/project.inlang/.gitignore b/project.inlang/.gitignore index 5e465967..04df3303 100644 --- a/project.inlang/.gitignore +++ b/project.inlang/.gitignore @@ -1 +1,19 @@ -cache \ No newline at end of file +# IF GIT SHOWED THAT THIS FILE CHANGED +# +# 1. RUN THE FOLLOWING COMMAND +# +# --- +# git rm --cached '**/*.inlang/.gitignore' +# --- +# +# 2. COMMIT THE CHANGE +# +# --- +# git commit -m "fix: remove tracked .gitignore from inlang project" +# --- +# +# Inlang handles the gitignore itself starting with version ^2.5. +# +# everything is ignored except settings.json +* +!settings.json \ No newline at end of file diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 40922c58..c51d7514 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -1,7 +1,7 @@ { "$schema": "https://inlang.com/schema/project-settings", - "baseLocale": "de", - "locales": ["de", "en"], + "baseLocale": "en", + "locales": ["en", "de", "pt"], "modules": [ "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", diff --git a/schema.graphql b/schema.graphql index 9113a95e..6c1a7c7b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -4,6 +4,7 @@ type AgendaItem { createdAt: DateTime! id: ID! isActive: Boolean + resolutionPapers(limit: Int, offset: Int, where: ResolutionPaperWhereInputArgument): [ResolutionPaper!]! speakersList(limit: Int, offset: Int, where: SpeakersListWhereInputArgument): [SpeakersList!]! title: String! updatedAt: DateTime @@ -14,34 +15,126 @@ input AgendaItemWhereInputArgument { committeeId: ID createdAt: DateTime id: ID + resolutionPapers: ResolutionPaperWhereInputArgument speakersList: SpeakersListWhereInputArgument title: String updatedAt: DateTime } +type Amendment { + createdAt: DateTime! + documentNumber: String + id: ID! + newContent: JSON + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + proposer(where: CommitteeMemberWhereInputArgument): CommitteeMember! + proposerCommitteeMemberId: ID! + sequenceNumber: Int + sponsors(limit: Int, offset: Int, where: AmendmentSponsorWhereInputArgument): [AmendmentSponsor!]! + status: AmendmentStatusEnum! + targetClauseId: ID + targetOperativeIndex: Int + targetPosition: Int + type: AmendmentTypeEnum! + updatedAt: DateTime +} + +type AmendmentSponsor { + amendment(where: AmendmentWhereInputArgument): Amendment! + amendmentId: ID! + committeeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember! + committeeMemberId: ID! + createdAt: DateTime! + id: ID! + updatedAt: DateTime +} + +input AmendmentSponsorWhereInputArgument { + amendment: AmendmentWhereInputArgument + amendmentId: ID + committeeMember: CommitteeMemberWhereInputArgument + committeeMemberId: ID + createdAt: DateTime + id: ID + updatedAt: DateTime +} + +enum AmendmentStatusEnum { + ACCEPTED + CONSENSUS_ADOPTED + PENDING + REJECTED + SUBMITTED + WITHDRAWN +} + +enum AmendmentTypeEnum { + ADD + ALTER_POSITION + ALTER_TEXT + DELETE +} + +input AmendmentWhereInputArgument { + createdAt: DateTime + documentNumber: String + id: ID + newContent: JSON + paper: ResolutionPaperWhereInputArgument + paperId: ID + proposer: CommitteeMemberWhereInputArgument + proposerCommitteeMemberId: ID + sequenceNumber: Int + sponsors: AmendmentSponsorWhereInputArgument + status: AmendmentStatusEnum + targetClauseId: ID + targetOperativeIndex: Int + targetPosition: Int + type: AmendmentTypeEnum + updatedAt: DateTime +} + +enum CommentVisibilityEnum { + PUBLIC + TEAM_ONLY +} + type Committee { abbreviation: String! activeAgendaItem(where: AgendaItemWhereInputArgument): AgendaItem activeAgendaItemId: ID + activeAmendment(where: AmendmentWhereInputArgument): Amendment + activeAmendmentId: ID + activeDraftResolution(where: ResolutionPaperWhereInputArgument): ResolutionPaper + activeDraftResolutionId: ID agendaItems(limit: Int, offset: Int, where: AgendaItemWhereInputArgument): [AgendaItem!]! allowDelegationsToAddThemselvesToSpeakersList: Boolean! + amendmentSponsoringOpen: Boolean! + amendmentSubmissionOpen: Boolean! conference(where: ConferenceWhereInputArgument): Conference conferenceId: ID! createdAt: DateTime! + currentOperativeClauseId: ID + currentOperativeIndex: Int customPaperSupportThreshold: Int customSimpleMajority: Int customTwoThirdsMajority: Int id: ID! lastResolutionAdoptionDate: DateTime + maxDraftResolutions: Int! members(limit: Int, offset: Int, where: CommitteeMemberWhereInputArgument): [CommitteeMember!]! name: String! paperSupportThreshold: Int + resolutionHeadline: String + resolutionPapers(limit: Int, offset: Int, where: ResolutionPaperWhereInputArgument): [ResolutionPaper!]! showWhiteboard: Boolean! simpleMajority: Int stateOfDebate: String status: CommitteeStatusEnum! statusHeadline: String! statusUntil: DateTime! + supportReEvaluationOpen: Boolean! totalPresent: Int twoThirdsMajority: Int updatedAt: DateTime @@ -49,27 +142,37 @@ type Committee { } type CommitteeMember { + amendmentSponsors(limit: Int, offset: Int, where: AmendmentSponsorWhereInputArgument): [AmendmentSponsor!]! + committee(where: CommitteeWhereInputArgument): Committee! committeeId: ID! createdAt: DateTime! + createdPapers(limit: Int, offset: Int, where: ResolutionPaperWhereInputArgument): [ResolutionPaper!]! id: ID! + paperSponsors(limit: Int, offset: Int, where: PaperSponsorWhereInputArgument): [PaperSponsor!]! presenceChangedTimestamps(limit: Int, offset: Int, where: PresenceChangedTimestampWhereInputArgument): [PresenceChangedTimestamp!]! present: Boolean! + proposedAmendments(limit: Int, offset: Int, where: AmendmentWhereInputArgument): [Amendment!]! representation(where: RepresentationWhereInputArgument): Representation! representationId: ID! updatedAt: DateTime - user(where: ConferenceUserWhereInputArgument): ConferenceUser + users(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! } input CommitteeMemberWhereInputArgument { + amendmentSponsors: AmendmentSponsorWhereInputArgument + committee: CommitteeWhereInputArgument committeeId: ID createdAt: DateTime + createdPapers: ResolutionPaperWhereInputArgument id: ID + paperSponsors: PaperSponsorWhereInputArgument presenceChangedTimestamps: PresenceChangedTimestampWhereInputArgument present: Boolean + proposedAmendments: AmendmentWhereInputArgument representation: RepresentationWhereInputArgument representationId: ID updatedAt: DateTime - user: ConferenceUserWhereInputArgument + users: ConferenceUserWhereInputArgument } enum CommitteeStatusEnum { @@ -84,23 +187,35 @@ input CommitteeWhereInputArgument { abbreviation: String activeAgendaItem: AgendaItemWhereInputArgument activeAgendaItemId: ID + activeAmendment: AmendmentWhereInputArgument + activeAmendmentId: ID + activeDraftResolution: ResolutionPaperWhereInputArgument + activeDraftResolutionId: ID agendaItems: AgendaItemWhereInputArgument allowDelegationsToAddThemselvesToSpeakersList: Boolean + amendmentSponsoringOpen: Boolean + amendmentSubmissionOpen: Boolean conference: ConferenceWhereInputArgument conferenceId: ID createdAt: DateTime + currentOperativeClauseId: ID + currentOperativeIndex: Int customPaperSupportThreshold: Int customSimpleMajority: Int customTwoThirdsMajority: Int id: ID lastResolutionAdoptionDate: DateTime + maxDraftResolutions: Int members: CommitteeMemberWhereInputArgument name: String + resolutionHeadline: String + resolutionPapers: ResolutionPaperWhereInputArgument showWhiteboard: Boolean stateOfDebate: String status: CommitteeStatusEnum statusHeadline: String statusUntil: DateTime + supportReEvaluationOpen: Boolean updatedAt: DateTime whiteboardContent: String } @@ -113,6 +228,7 @@ type Conference { members(limit: Int, offset: Int, where: ConferenceMemberWhereInputArgument): [ConferenceMember!]! pressWebsite: String representations(limit: Int, offset: Int, where: RepresentationWhereInputArgument): [Representation!]! + resolutionFeatureEnabled: Boolean! title: String! """ @@ -132,7 +248,7 @@ type ConferenceMember { representationId: ID! speakerOnList(limit: Int, offset: Int, where: SpeakerOnListWhereInputArgument): [SpeakerOnList!]! updatedAt: DateTime - user(where: ConferenceUserWhereInputArgument): ConferenceUser + users(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! } input ConferenceMemberWhereInputArgument { @@ -144,17 +260,22 @@ input ConferenceMemberWhereInputArgument { representationId: ID speakerOnList: SpeakerOnListWhereInputArgument updatedAt: DateTime - user: ConferenceUserWhereInputArgument + users: ConferenceUserWhereInputArgument } type ConferenceUser { + clauseLocks(limit: Int, offset: Int, where: PaperClauseLockWhereInputArgument): [PaperClauseLock!]! + comments(limit: Int, offset: Int, where: ResolutionCommentWhereInputArgument): [ResolutionComment!]! + committeeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember committeeMemberId: ID conference(where: ConferenceWhereInputArgument): Conference! conferenceId: ID! + conferenceMember(where: ConferenceMemberWhereInputArgument): ConferenceMember conferenceMemberId: ID conferenceUserType: ConferenceUserTypeEnum! createdAt: DateTime! id: ID! + paperEditors(limit: Int, offset: Int, where: PaperEditorWhereInputArgument): [PaperEditor!]! updatedAt: DateTime user(where: UserWhereInputArgument): User userEmail: String! @@ -169,13 +290,18 @@ enum ConferenceUserTypeEnum { } input ConferenceUserWhereInputArgument { + clauseLocks: PaperClauseLockWhereInputArgument + comments: ResolutionCommentWhereInputArgument + committeeMember: CommitteeMemberWhereInputArgument committeeMemberId: ID conference: ConferenceWhereInputArgument conferenceId: ID + conferenceMember: ConferenceMemberWhereInputArgument conferenceMemberId: ID conferenceUserType: ConferenceUserTypeEnum createdAt: DateTime id: ID + paperEditors: PaperEditorWhereInputArgument updatedAt: DateTime user: UserWhereInputArgument userEmail: String @@ -189,6 +315,7 @@ input ConferenceWhereInputArgument { members: ConferenceMemberWhereInputArgument pressWebsite: String representations: RepresentationWhereInputArgument + resolutionFeatureEnabled: Boolean title: String updatedAt: DateTime users: ConferenceUserWhereInputArgument @@ -226,6 +353,7 @@ input ImportDataCommittee { abbreviation: String! id: ID! name: String! + resolutionHeadline: String } input ImportDataCommitteeMember { @@ -263,19 +391,203 @@ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http:// scalar JSON type Mutation { + acceptAmendment(amendmentId: ID!): Amendment + acquireClauseLock(clauseId: String!, paperId: ID!): PaperClauseLock + addAmendmentSponsor(amendmentId: ID!, committeeMemberId: ID!): AmendmentSponsor addSpeakerOnList(committeeMemberId: ID, conferenceMemberId: ID, position: Int, speakersListId: ID!): SpeakerOnList + addSponsor(committeeMemberId: ID!, paperId: ID!): PaperSponsor + adoptByConsensus(amendmentId: ID!): Amendment + chairCreateAmendment(committeeMemberId: ID!, newContent: JSON, paperId: ID!, targetClauseId: String, targetOperativeIndex: Int, targetPosition: Int, type: AmendmentTypeEnum!): Amendment + chairCreateResolutionPaper(agendaItemId: ID!, committeeId: ID!, committeeMemberId: ID!, title: String): ResolutionPaper clearSpeakersList(id: ID!): SpeakersList createAgendaItem(committeeId: ID!, title: String!): AgendaItem + createAmendment(newContent: JSON, paperId: ID!, targetClauseId: String, targetOperativeIndex: Int, targetPosition: Int, type: AmendmentTypeEnum!): Amendment + createComment(clauseId: String, content: String!, paperId: ID!, parentCommentId: ID, visibility: CommentVisibilityEnum): ResolutionComment + createCommittee(abbreviation: String!, conferenceId: ID!, name: String!): Committee + createCommitteeMember(committeeId: ID!, representationId: ID!): CommitteeMember + createConferenceMember(conferenceId: ID!, representationId: ID!): ConferenceMember createConferenceUser(conferenceId: ID!, conferenceUserType: ConferenceUserTypeEnum!, userEmail: String!): ConferenceUser + createRepresentation(alpha2Code: String, alpha3Code: String, conferenceId: ID!, faIcon: String, name: String, type: RepresentationTypeEnum!): Representation + createResolutionPaper(agendaItemId: ID!, committeeId: ID!, title: String): ResolutionPaper + createShareCode(paperId: ID!, permission: ShareCodePermissionEnum!): PaperShareCode + deleteClauseVote(clauseId: String!, paperId: ID!): Boolean + deleteComment(commentId: ID!): Boolean + deleteCommittee(id: ID!): Boolean + deleteCommitteeMember(id: ID!): Boolean + deleteConference(id: ID!): Boolean + deleteConferenceMember(id: ID!): Boolean deleteConferenceUser(id: ID!): Boolean + deleteRepresentation(id: ID!): Boolean + deleteShareCode(shareCodeId: ID!): Boolean + editAmendment(amendmentId: ID!, newContent: JSON, proposerCommitteeMemberId: ID, targetClauseId: String, targetOperativeIndex: Int, targetPosition: Int): Amendment importDelegatorConference(data: ImportData!): Conference moveSpeakerToPosition(id: ID!, position: Int!): SpeakerOnList + promoteToDraftResolution(paperId: ID!): ResolutionPaper + recordClauseVote(clauseId: String!, outcome: VoteOutcomeEnum!, paperId: ID!, votesAbstain: Int, votesAgainst: Int!, votesFor: Int!): OperativeClauseVote + recordVoteResult(outcome: VoteOutcomeEnum!, paperId: ID!, votesAbstain: Int, votesAgainst: Int!, votesFor: Int!): ResolutionPaper + redeemShareCode(code: String!): ShareCodeRedemptionResult + rejectAmendment(amendmentId: ID!): Amendment + releaseAllMyLocks(paperId: ID!): Boolean + releaseClauseLock(clauseId: String!, paperId: ID!): Boolean + removeAmendmentSponsor(amendmentId: ID!, committeeMemberId: ID!): Boolean + removeEditor(conferenceUserId: ID!, paperId: ID!): Boolean removeSpeakerOnList(speakerOnListId: ID!): SpeakersList + removeSponsor(committeeMemberId: ID!, paperId: ID!): Boolean + revertPaperStatus(paperId: ID!, restoreSnapshot: Boolean): ResolutionPaper + selfAddToSpeakersList(speakersListId: ID!): SpeakerOnList + selfRemoveFromSpeakersList(speakersListId: ID!): SpeakersList setPresenceForCommitteeMembers(ids: [ID!]!, present: Boolean!): [CommitteeMember!] - updateCommittee(activeAgendaItemId: ID, id: ID!, lastResolutionAdoptionDate: DateTime, showWhiteboard: Boolean, stateOfDebate: String, status: CommitteeStatusEnum, statusHeadline: String, statusUntil: DateTime, whiteboardContent: String): Committee - updateConferenceUser(conferenceUserType: ConferenceUserTypeEnum!, id: ID!): ConferenceUser + softDeletePaper(paperId: ID!): Boolean + startVotingPhase(paperId: ID!): ResolutionPaper + submitPaper(paperId: ID!): ResolutionPaper + updateComment(commentId: ID!, content: String!): ResolutionComment + updateCommittee(abbreviation: String, activeAgendaItemId: ID, activeAmendmentId: ID, activeDraftResolutionId: ID, allowDelegationsToAddThemselvesToSpeakersList: Boolean, amendmentSponsoringOpen: Boolean, amendmentSubmissionOpen: Boolean, clearActiveAmendment: Boolean, clearActiveDraftResolution: Boolean, currentOperativeClauseId: String, currentOperativeIndex: Int, id: ID!, lastResolutionAdoptionDate: DateTime, maxDraftResolutions: Int, name: String, showWhiteboard: Boolean, stateOfDebate: String, status: CommitteeStatusEnum, statusHeadline: String, statusUntil: DateTime, supportReEvaluationOpen: Boolean, whiteboardContent: String): Committee + updateConference(hasModeratedCaucus: Boolean, id: ID!, pressWebsite: String, resolutionFeatureEnabled: Boolean, title: String): Conference + updateConferenceUser(committeeMemberId: ID, conferenceMemberId: ID, conferenceUserType: ConferenceUserTypeEnum!, id: ID!): ConferenceUser + updatePaperContent(content: JSON!, paperId: ID!): ResolutionPaper + updatePaperTitle(paperId: ID!, title: String!): ResolutionPaper updateSpeakerOnList(id: ID!, overwriteName: String): SpeakerOnList updateSpeakersList(id: ID!, isClosed: Boolean, speakingTime: Int, startTimestamp: DateTime, stopTimer: Boolean = false, timeLeft: Int): SpeakersList + withdrawAmendment(amendmentId: ID!): Amendment +} + +type OperativeClauseVote { + clauseId: ID! + createdAt: DateTime! + id: ID! + outcome: VoteOutcomeEnum! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + updatedAt: DateTime + votesAbstain: Int! + votesAgainst: Int! + votesFor: Int! +} + +input OperativeClauseVoteWhereInputArgument { + clauseId: ID + createdAt: DateTime + id: ID + outcome: VoteOutcomeEnum + paper: ResolutionPaperWhereInputArgument + paperId: ID + updatedAt: DateTime + votesAbstain: Int + votesAgainst: Int + votesFor: Int +} + +type PaperClauseLock { + acquiredAt: DateTime! + clauseId: ID! + conferenceUser(where: ConferenceUserWhereInputArgument): ConferenceUser! + conferenceUserId: ID! + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + updatedAt: DateTime +} + +input PaperClauseLockWhereInputArgument { + acquiredAt: DateTime + clauseId: ID + conferenceUser: ConferenceUserWhereInputArgument + conferenceUserId: ID + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + updatedAt: DateTime +} + +type PaperContentSnapshot { + content: JSON + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + trigger: String + updatedAt: DateTime +} + +input PaperContentSnapshotWhereInputArgument { + content: JSON + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + trigger: String + updatedAt: DateTime +} + +type PaperEditor { + conferenceUser(where: ConferenceUserWhereInputArgument): ConferenceUser! + conferenceUserId: ID! + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + updatedAt: DateTime +} + +input PaperEditorWhereInputArgument { + conferenceUser: ConferenceUserWhereInputArgument + conferenceUserId: ID + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + updatedAt: DateTime +} + +type PaperShareCode { + code: String! + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + permission: ShareCodePermissionEnum! + updatedAt: DateTime +} + +input PaperShareCodeWhereInputArgument { + code: String + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + permission: ShareCodePermissionEnum + updatedAt: DateTime +} + +type PaperSponsor { + committeeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember! + committeeMemberId: ID! + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + updatedAt: DateTime +} + +input PaperSponsorWhereInputArgument { + committeeMember: CommitteeMemberWhereInputArgument + committeeMemberId: ID + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + updatedAt: DateTime +} + +enum PaperStatusEnum { + AMENDMENT_PHASE + DRAFT_RESOLUTION + FINAL + SUBMITTED + VOTING_PHASE + WORKING_PAPER } type PresenceChangedTimestamp { @@ -300,27 +612,50 @@ input PresenceChangedTimestampWhereInputArgument { type Query { findFirstAgendaItem(where: AgendaItemWhereInputArgument): AgendaItem! + findFirstAmendment(where: AmendmentWhereInputArgument): Amendment! + findFirstAmendmentSponsor(where: AmendmentSponsorWhereInputArgument): AmendmentSponsor! findFirstCommittee(where: CommitteeWhereInputArgument): Committee! findFirstCommitteeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember! findFirstConference(where: ConferenceWhereInputArgument): Conference! findFirstConferenceMember(where: ConferenceMemberWhereInputArgument): ConferenceMember! findFirstConferenceUser(where: ConferenceUserWhereInputArgument): ConferenceUser! + findFirstOperativeClauseVote(where: OperativeClauseVoteWhereInputArgument): OperativeClauseVote! + findFirstPaperClauseLock(where: PaperClauseLockWhereInputArgument): PaperClauseLock! + findFirstPaperContentSnapshot(where: PaperContentSnapshotWhereInputArgument): PaperContentSnapshot! + findFirstPaperEditor(where: PaperEditorWhereInputArgument): PaperEditor! + findFirstPaperShareCode(where: PaperShareCodeWhereInputArgument): PaperShareCode! + findFirstPaperSponsor(where: PaperSponsorWhereInputArgument): PaperSponsor! findFirstPresenceChangedTimestamp(where: PresenceChangedTimestampWhereInputArgument): PresenceChangedTimestamp! findFirstRepresentation(where: RepresentationWhereInputArgument): Representation! + findFirstResolutionComment(where: ResolutionCommentWhereInputArgument): ResolutionComment! + findFirstResolutionPaper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + findFirstResolutionVoteResult(where: ResolutionVoteResultWhereInputArgument): ResolutionVoteResult! findFirstSpeakerOnList(where: SpeakerOnListWhereInputArgument): SpeakerOnList! findFirstSpeakersList(where: SpeakersListWhereInputArgument): SpeakersList! findFirstUser(where: UserWhereInputArgument): User! findManyAgendaItem(limit: Int, offset: Int, where: AgendaItemWhereInputArgument): [AgendaItem!]! + findManyAmendment(limit: Int, offset: Int, where: AmendmentWhereInputArgument): [Amendment!]! + findManyAmendmentSponsor(limit: Int, offset: Int, where: AmendmentSponsorWhereInputArgument): [AmendmentSponsor!]! findManyCommittee(limit: Int, offset: Int, where: CommitteeWhereInputArgument): [Committee!]! findManyCommitteeMember(limit: Int, offset: Int, where: CommitteeMemberWhereInputArgument): [CommitteeMember!]! findManyConference(limit: Int, offset: Int, where: ConferenceWhereInputArgument): [Conference!]! findManyConferenceMember(limit: Int, offset: Int, where: ConferenceMemberWhereInputArgument): [ConferenceMember!]! findManyConferenceUser(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! + findManyOperativeClauseVote(limit: Int, offset: Int, where: OperativeClauseVoteWhereInputArgument): [OperativeClauseVote!]! + findManyPaperClauseLock(limit: Int, offset: Int, where: PaperClauseLockWhereInputArgument): [PaperClauseLock!]! + findManyPaperContentSnapshot(limit: Int, offset: Int, where: PaperContentSnapshotWhereInputArgument): [PaperContentSnapshot!]! + findManyPaperEditor(limit: Int, offset: Int, where: PaperEditorWhereInputArgument): [PaperEditor!]! + findManyPaperShareCode(limit: Int, offset: Int, where: PaperShareCodeWhereInputArgument): [PaperShareCode!]! + findManyPaperSponsor(limit: Int, offset: Int, where: PaperSponsorWhereInputArgument): [PaperSponsor!]! findManyPresenceChangedTimestamp(limit: Int, offset: Int, where: PresenceChangedTimestampWhereInputArgument): [PresenceChangedTimestamp!]! findManyRepresentation(limit: Int, offset: Int, where: RepresentationWhereInputArgument): [Representation!]! + findManyResolutionComment(limit: Int, offset: Int, where: ResolutionCommentWhereInputArgument): [ResolutionComment!]! + findManyResolutionPaper(limit: Int, offset: Int, where: ResolutionPaperWhereInputArgument): [ResolutionPaper!]! + findManyResolutionVoteResult(limit: Int, offset: Int, where: ResolutionVoteResultWhereInputArgument): [ResolutionVoteResult!]! findManySpeakerOnList(limit: Int, offset: Int, where: SpeakerOnListWhereInputArgument): [SpeakerOnList!]! findManySpeakersList(limit: Int, offset: Int, where: SpeakersListWhereInputArgument): [SpeakersList!]! findManyUser(limit: Int, offset: Int, where: UserWhereInputArgument): [User!]! + isGlobalAdmin: Boolean serverTime: DateTime! } @@ -370,6 +705,126 @@ input RepresentationWhereInputArgument { updatedAt: DateTime } +type ResolutionComment { + author(where: ConferenceUserWhereInputArgument): ConferenceUser! + authorConferenceUserId: ID! + clauseId: ID + content: String! + createdAt: DateTime! + id: ID! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + parentComment(where: ResolutionCommentWhereInputArgument): ResolutionComment + parentCommentId: ID + replies(limit: Int, offset: Int, where: ResolutionCommentWhereInputArgument): [ResolutionComment!]! + updatedAt: DateTime + visibility: CommentVisibilityEnum! +} + +input ResolutionCommentWhereInputArgument { + author: ConferenceUserWhereInputArgument + authorConferenceUserId: ID + clauseId: ID + content: String + createdAt: DateTime + id: ID + paper: ResolutionPaperWhereInputArgument + paperId: ID + parentComment: ResolutionCommentWhereInputArgument + parentCommentId: ID + replies: ResolutionCommentWhereInputArgument + updatedAt: DateTime + visibility: CommentVisibilityEnum +} + +type ResolutionPaper { + agendaItem(where: AgendaItemWhereInputArgument): AgendaItem! + agendaItemId: ID! + amendments(limit: Int, offset: Int, where: AmendmentWhereInputArgument): [Amendment!]! + clauseLocks(limit: Int, offset: Int, where: PaperClauseLockWhereInputArgument): [PaperClauseLock!]! + comments(limit: Int, offset: Int, where: ResolutionCommentWhereInputArgument): [ResolutionComment!]! + committee(where: CommitteeWhereInputArgument): Committee! + committeeId: ID! + content: JSON + createdAt: DateTime! + creator(where: CommitteeMemberWhereInputArgument): CommitteeMember! + creatorCommitteeMemberId: ID! + deletedAt: DateTime + documentNumber: String + editors(limit: Int, offset: Int, where: PaperEditorWhereInputArgument): [PaperEditor!]! + id: ID! + operativeClauseVotes(limit: Int, offset: Int, where: OperativeClauseVoteWhereInputArgument): [OperativeClauseVote!]! + sequenceNumber: Int + shareCodes(limit: Int, offset: Int, where: PaperShareCodeWhereInputArgument): [PaperShareCode!]! + snapshots(limit: Int, offset: Int, where: PaperContentSnapshotWhereInputArgument): [PaperContentSnapshot!]! + sponsors(limit: Int, offset: Int, where: PaperSponsorWhereInputArgument): [PaperSponsor!]! + status: PaperStatusEnum! + title: String + updatedAt: DateTime + voteResult(where: ResolutionVoteResultWhereInputArgument): ResolutionVoteResult +} + +input ResolutionPaperWhereInputArgument { + agendaItem: AgendaItemWhereInputArgument + agendaItemId: ID + amendments: AmendmentWhereInputArgument + clauseLocks: PaperClauseLockWhereInputArgument + comments: ResolutionCommentWhereInputArgument + committee: CommitteeWhereInputArgument + committeeId: ID + content: JSON + createdAt: DateTime + creator: CommitteeMemberWhereInputArgument + creatorCommitteeMemberId: ID + deletedAt: DateTime + documentNumber: String + editors: PaperEditorWhereInputArgument + id: ID + operativeClauseVotes: OperativeClauseVoteWhereInputArgument + sequenceNumber: Int + shareCodes: PaperShareCodeWhereInputArgument + snapshots: PaperContentSnapshotWhereInputArgument + sponsors: PaperSponsorWhereInputArgument + status: PaperStatusEnum + title: String + updatedAt: DateTime + voteResult: ResolutionVoteResultWhereInputArgument +} + +type ResolutionVoteResult { + createdAt: DateTime! + id: ID! + outcome: VoteOutcomeEnum! + paper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + paperId: ID! + updatedAt: DateTime + votesAbstain: Int! + votesAgainst: Int! + votesFor: Int! +} + +input ResolutionVoteResultWhereInputArgument { + createdAt: DateTime + id: ID + outcome: VoteOutcomeEnum + paper: ResolutionPaperWhereInputArgument + paperId: ID + updatedAt: DateTime + votesAbstain: Int + votesAgainst: Int + votesFor: Int +} + +enum ShareCodePermissionEnum { + EDIT + SPONSOR +} + +type ShareCodeRedemptionResult { + paperId: ID + permission: String +} + type SpeakerOnList { committeeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember committeeMemberId: ID @@ -433,24 +888,46 @@ input SpeakersListWhereInputArgument { type Subscription { findFirstAgendaItem(where: AgendaItemWhereInputArgument): AgendaItem! + findFirstAmendment(where: AmendmentWhereInputArgument): Amendment! + findFirstAmendmentSponsor(where: AmendmentSponsorWhereInputArgument): AmendmentSponsor! findFirstCommittee(where: CommitteeWhereInputArgument): Committee! findFirstCommitteeMember(where: CommitteeMemberWhereInputArgument): CommitteeMember! findFirstConference(where: ConferenceWhereInputArgument): Conference! findFirstConferenceMember(where: ConferenceMemberWhereInputArgument): ConferenceMember! findFirstConferenceUser(where: ConferenceUserWhereInputArgument): ConferenceUser! + findFirstOperativeClauseVote(where: OperativeClauseVoteWhereInputArgument): OperativeClauseVote! + findFirstPaperClauseLock(where: PaperClauseLockWhereInputArgument): PaperClauseLock! + findFirstPaperContentSnapshot(where: PaperContentSnapshotWhereInputArgument): PaperContentSnapshot! + findFirstPaperEditor(where: PaperEditorWhereInputArgument): PaperEditor! + findFirstPaperShareCode(where: PaperShareCodeWhereInputArgument): PaperShareCode! + findFirstPaperSponsor(where: PaperSponsorWhereInputArgument): PaperSponsor! findFirstPresenceChangedTimestamp(where: PresenceChangedTimestampWhereInputArgument): PresenceChangedTimestamp! findFirstRepresentation(where: RepresentationWhereInputArgument): Representation! + findFirstResolutionComment(where: ResolutionCommentWhereInputArgument): ResolutionComment! + findFirstResolutionPaper(where: ResolutionPaperWhereInputArgument): ResolutionPaper! + findFirstResolutionVoteResult(where: ResolutionVoteResultWhereInputArgument): ResolutionVoteResult! findFirstSpeakerOnList(where: SpeakerOnListWhereInputArgument): SpeakerOnList! findFirstSpeakersList(where: SpeakersListWhereInputArgument): SpeakersList! findFirstUser(where: UserWhereInputArgument): User! findManyAgendaItem(limit: Int, offset: Int, where: AgendaItemWhereInputArgument): [AgendaItem!]! + findManyAmendment(limit: Int, offset: Int, where: AmendmentWhereInputArgument): [Amendment!]! + findManyAmendmentSponsor(limit: Int, offset: Int, where: AmendmentSponsorWhereInputArgument): [AmendmentSponsor!]! findManyCommittee(limit: Int, offset: Int, where: CommitteeWhereInputArgument): [Committee!]! findManyCommitteeMember(limit: Int, offset: Int, where: CommitteeMemberWhereInputArgument): [CommitteeMember!]! findManyConference(limit: Int, offset: Int, where: ConferenceWhereInputArgument): [Conference!]! findManyConferenceMember(limit: Int, offset: Int, where: ConferenceMemberWhereInputArgument): [ConferenceMember!]! findManyConferenceUser(limit: Int, offset: Int, where: ConferenceUserWhereInputArgument): [ConferenceUser!]! + findManyOperativeClauseVote(limit: Int, offset: Int, where: OperativeClauseVoteWhereInputArgument): [OperativeClauseVote!]! + findManyPaperClauseLock(limit: Int, offset: Int, where: PaperClauseLockWhereInputArgument): [PaperClauseLock!]! + findManyPaperContentSnapshot(limit: Int, offset: Int, where: PaperContentSnapshotWhereInputArgument): [PaperContentSnapshot!]! + findManyPaperEditor(limit: Int, offset: Int, where: PaperEditorWhereInputArgument): [PaperEditor!]! + findManyPaperShareCode(limit: Int, offset: Int, where: PaperShareCodeWhereInputArgument): [PaperShareCode!]! + findManyPaperSponsor(limit: Int, offset: Int, where: PaperSponsorWhereInputArgument): [PaperSponsor!]! findManyPresenceChangedTimestamp(limit: Int, offset: Int, where: PresenceChangedTimestampWhereInputArgument): [PresenceChangedTimestamp!]! findManyRepresentation(limit: Int, offset: Int, where: RepresentationWhereInputArgument): [Representation!]! + findManyResolutionComment(limit: Int, offset: Int, where: ResolutionCommentWhereInputArgument): [ResolutionComment!]! + findManyResolutionPaper(limit: Int, offset: Int, where: ResolutionPaperWhereInputArgument): [ResolutionPaper!]! + findManyResolutionVoteResult(limit: Int, offset: Int, where: ResolutionVoteResultWhereInputArgument): [ResolutionVoteResult!]! findManySpeakerOnList(limit: Int, offset: Int, where: SpeakerOnListWhereInputArgument): [SpeakerOnList!]! findManySpeakersList(limit: Int, offset: Int, where: SpeakersListWhereInputArgument): [SpeakersList!]! findManyUser(limit: Int, offset: Int, where: UserWhereInputArgument): [User!]! @@ -478,4 +955,10 @@ input UserWhereInputArgument { locale: String preferredUsername: String updatedAt: DateTime +} + +enum VoteOutcomeEnum { + ADOPTED + REJECTED + SENT_BACK } \ No newline at end of file diff --git a/src/api/context.ts b/src/api/context.ts index 35f67148..d3bc1a9f 100644 --- a/src/api/context.ts +++ b/src/api/context.ts @@ -1,5 +1,6 @@ import { configPrivate } from '$config/private'; import type { RequestEvent } from '@sveltejs/kit'; +import { GraphQLError } from 'graphql'; export const oidcRoles = ['admin', 'member', 'service_user'] as const; @@ -8,11 +9,17 @@ export async function context(req: RequestEvent) { if (configPrivate.OIDC_ROLE_CLAIM) { const rolesRaw = (req.locals.oidc?.accessToken ?? ({} as any))[configPrivate.OIDC_ROLE_CLAIM] ?? - (req.locals.oidc?.idToken ?? ({} as any))[configPrivate.OIDC_ROLE_CLAIM] ?? - {}; + (req.locals.oidc?.idToken ?? ({} as any))[configPrivate.OIDC_ROLE_CLAIM]; if (rolesRaw) { - const roleNames = Object.keys(rolesRaw); - OIDCRoleNames.push(...(roleNames as any)); + // Support both Logto format (array of role objects/strings) and Zitadel format (object with role keys) + if (Array.isArray(rolesRaw)) { + for (const role of rolesRaw) { + const name = typeof role === 'string' ? role : role?.name; + if (name) OIDCRoleNames.push(name as any); + } + } else if (typeof rolesRaw === 'object') { + OIDCRoleNames.push(...(Object.keys(rolesRaw) as any)); + } } } @@ -20,7 +27,7 @@ export async function context(req: RequestEvent) { ...req.locals, mustBeLoggedIn: () => { if (!req.locals.oidc?.user) { - throw new Error('Must be logged in'); + throw new GraphQLError('Must be logged in'); } return req.locals.oidc.user; diff --git a/src/api/db/relations.ts b/src/api/db/relations.ts index 3bd526b4..b61892d6 100644 --- a/src/api/db/relations.ts +++ b/src/api/db/relations.ts @@ -35,6 +35,14 @@ export const relations = defineRelations(schema, (r) => ({ from: r.committee.activeAgendaItemId, to: r.agendaItem.id }), + activeDraftResolution: r.one.resolutionPaper({ + from: r.committee.activeDraftResolutionId, + to: r.resolutionPaper.id + }), + activeAmendment: r.one.amendment({ + from: r.committee.activeAmendmentId, + to: r.amendment.id + }), agendaItems: r.many.agendaItem({ from: r.committee.id, to: r.agendaItem.committeeId @@ -42,6 +50,10 @@ export const relations = defineRelations(schema, (r) => ({ members: r.many.committeeMember({ from: r.committee.id, to: r.committeeMember.committeeId + }), + resolutionPapers: r.many.resolutionPaper({ + from: r.committee.id, + to: r.resolutionPaper.committeeId }) }, committeeMember: { @@ -50,14 +62,34 @@ export const relations = defineRelations(schema, (r) => ({ to: r.representation.id, optional: false }), - user: r.one.conferenceUser({ + committee: r.one.committee({ + from: r.committeeMember.committeeId, + to: r.committee.id, + optional: false + }), + users: r.many.conferenceUser({ from: r.committeeMember.id, - to: r.conferenceUser.committeeMemberId, - optional: true + to: r.conferenceUser.committeeMemberId }), presenceChangedTimestamps: r.many.presenceChangedTimestamp({ from: r.committeeMember.id, to: r.presenceChangedTimestamp.committeeMemberId + }), + createdPapers: r.many.resolutionPaper({ + from: r.committeeMember.id, + to: r.resolutionPaper.creatorCommitteeMemberId + }), + paperSponsors: r.many.paperSponsor({ + from: r.committeeMember.id, + to: r.paperSponsor.committeeMemberId + }), + amendmentSponsors: r.many.amendmentSponsor({ + from: r.committeeMember.id, + to: r.amendmentSponsor.committeeMemberId + }), + proposedAmendments: r.many.amendment({ + from: r.committeeMember.id, + to: r.amendment.proposerCommitteeMemberId }) }, conferenceUser: { @@ -69,6 +101,28 @@ export const relations = defineRelations(schema, (r) => ({ from: r.conferenceUser.conferenceId, to: r.conference.id, optional: false + }), + committeeMember: r.one.committeeMember({ + from: r.conferenceUser.committeeMemberId, + to: r.committeeMember.id, + optional: true + }), + conferenceMember: r.one.conferenceMember({ + from: r.conferenceUser.conferenceMemberId, + to: r.conferenceMember.id, + optional: true + }), + paperEditors: r.many.paperEditor({ + from: r.conferenceUser.id, + to: r.paperEditor.conferenceUserId + }), + comments: r.many.resolutionComment({ + from: r.conferenceUser.id, + to: r.resolutionComment.authorConferenceUserId + }), + clauseLocks: r.many.paperClauseLock({ + from: r.conferenceUser.id, + to: r.paperClauseLock.conferenceUserId }) }, representation: { @@ -99,10 +153,9 @@ export const relations = defineRelations(schema, (r) => ({ from: r.conferenceMember.id, to: r.speakerOnList.conferenceMemberId }), - user: r.one.conferenceUser({ + users: r.many.conferenceUser({ from: r.conferenceMember.id, - to: r.conferenceUser.conferenceMemberId, - optional: true + to: r.conferenceUser.conferenceMemberId }) }, agendaItem: { @@ -113,6 +166,10 @@ export const relations = defineRelations(schema, (r) => ({ speakersList: r.many.speakersList({ from: r.agendaItem.id, to: r.speakersList.agendaItemId + }), + resolutionPapers: r.many.resolutionPaper({ + from: r.agendaItem.id, + to: r.resolutionPaper.agendaItemId }) }, speakersList: { @@ -168,5 +225,170 @@ export const relations = defineRelations(schema, (r) => ({ from: r.presenceChangedTimestamp.committeeMemberId, to: r.committeeMember.id }) + }, + resolutionPaper: { + committee: r.one.committee({ + from: r.resolutionPaper.committeeId, + to: r.committee.id, + optional: false + }), + agendaItem: r.one.agendaItem({ + from: r.resolutionPaper.agendaItemId, + to: r.agendaItem.id, + optional: false + }), + creator: r.one.committeeMember({ + from: r.resolutionPaper.creatorCommitteeMemberId, + to: r.committeeMember.id, + optional: false + }), + sponsors: r.many.paperSponsor({ + from: r.resolutionPaper.id, + to: r.paperSponsor.paperId + }), + shareCodes: r.many.paperShareCode({ + from: r.resolutionPaper.id, + to: r.paperShareCode.paperId + }), + editors: r.many.paperEditor({ + from: r.resolutionPaper.id, + to: r.paperEditor.paperId + }), + comments: r.many.resolutionComment({ + from: r.resolutionPaper.id, + to: r.resolutionComment.paperId + }), + amendments: r.many.amendment({ + from: r.resolutionPaper.id, + to: r.amendment.paperId + }), + snapshots: r.many.paperContentSnapshot({ + from: r.resolutionPaper.id, + to: r.paperContentSnapshot.paperId + }), + operativeClauseVotes: r.many.operativeClauseVote({ + from: r.resolutionPaper.id, + to: r.operativeClauseVote.paperId + }), + voteResult: r.one.resolutionVoteResult({ + from: r.resolutionPaper.id, + to: r.resolutionVoteResult.paperId + }), + clauseLocks: r.many.paperClauseLock({ + from: r.resolutionPaper.id, + to: r.paperClauseLock.paperId + }) + }, + paperContentSnapshot: { + paper: r.one.resolutionPaper({ + from: r.paperContentSnapshot.paperId, + to: r.resolutionPaper.id, + optional: false + }) + }, + paperSponsor: { + paper: r.one.resolutionPaper({ + from: r.paperSponsor.paperId, + to: r.resolutionPaper.id, + optional: false + }), + committeeMember: r.one.committeeMember({ + from: r.paperSponsor.committeeMemberId, + to: r.committeeMember.id, + optional: false + }) + }, + paperShareCode: { + paper: r.one.resolutionPaper({ + from: r.paperShareCode.paperId, + to: r.resolutionPaper.id, + optional: false + }) + }, + paperEditor: { + paper: r.one.resolutionPaper({ + from: r.paperEditor.paperId, + to: r.resolutionPaper.id, + optional: false + }), + conferenceUser: r.one.conferenceUser({ + from: r.paperEditor.conferenceUserId, + to: r.conferenceUser.id, + optional: false + }) + }, + resolutionComment: { + paper: r.one.resolutionPaper({ + from: r.resolutionComment.paperId, + to: r.resolutionPaper.id, + optional: false + }), + author: r.one.conferenceUser({ + from: r.resolutionComment.authorConferenceUserId, + to: r.conferenceUser.id, + optional: false + }), + parentComment: r.one.resolutionComment({ + from: r.resolutionComment.parentCommentId, + to: r.resolutionComment.id + }), + replies: r.many.resolutionComment({ + from: r.resolutionComment.id, + to: r.resolutionComment.parentCommentId + }) + }, + amendment: { + paper: r.one.resolutionPaper({ + from: r.amendment.paperId, + to: r.resolutionPaper.id, + optional: false + }), + proposer: r.one.committeeMember({ + from: r.amendment.proposerCommitteeMemberId, + to: r.committeeMember.id, + optional: false + }), + sponsors: r.many.amendmentSponsor({ + from: r.amendment.id, + to: r.amendmentSponsor.amendmentId + }) + }, + amendmentSponsor: { + amendment: r.one.amendment({ + from: r.amendmentSponsor.amendmentId, + to: r.amendment.id, + optional: false + }), + committeeMember: r.one.committeeMember({ + from: r.amendmentSponsor.committeeMemberId, + to: r.committeeMember.id, + optional: false + }) + }, + operativeClauseVote: { + paper: r.one.resolutionPaper({ + from: r.operativeClauseVote.paperId, + to: r.resolutionPaper.id, + optional: false + }) + }, + resolutionVoteResult: { + paper: r.one.resolutionPaper({ + from: r.resolutionVoteResult.paperId, + to: r.resolutionPaper.id, + optional: false + }) + }, + paperClauseLock: { + paper: r.one.resolutionPaper({ + from: r.paperClauseLock.paperId, + to: r.resolutionPaper.id, + optional: false + }), + conferenceUser: r.one.conferenceUser({ + from: r.paperClauseLock.conferenceUserId, + to: r.conferenceUser.id, + optional: false + }) } })); diff --git a/src/api/db/reset.ts b/src/api/db/reset.ts index 612bcc72..9a82fb10 100644 --- a/src/api/db/reset.ts +++ b/src/api/db/reset.ts @@ -1,12 +1,28 @@ +import { sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/node-postgres'; -import * as schema from './schema'; -import { reset } from 'drizzle-seed'; const db = drizzle(process.env.DATABASE_URL!, { - schema: schema, casing: 'snake_case' }); console.info('Resetting database...'); -await reset(db, schema); +await db.execute(sql` + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public') LOOP + EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE'; + END LOOP; + END $$; +`); +// Also drop custom enum types +await db.execute(sql` + DO $$ DECLARE + r RECORD; + BEGIN + FOR r IN (SELECT typname FROM pg_type t JOIN pg_namespace n ON t.typnamespace = n.oid WHERE n.nspname = 'public' AND t.typtype = 'e') LOOP + EXECUTE 'DROP TYPE IF EXISTS public.' || quote_ident(r.typname) || ' CASCADE'; + END LOOP; + END $$; +`); console.info('Resetting database done.'); diff --git a/src/api/db/schema.ts b/src/api/db/schema.ts index 9e33bb8f..aa32d1b0 100644 --- a/src/api/db/schema.ts +++ b/src/api/db/schema.ts @@ -7,6 +7,8 @@ import { pgEnum, boolean, smallint, + integer, + json, type AnyPgColumn } from 'drizzle-orm/pg-core'; @@ -40,7 +42,8 @@ export const conference = pgTable('conference', { ...defaultIdAndTimestamps, title: text().notNull(), pressWebsite: text(), - hasModeratedCaucus: boolean().notNull().default(false) + hasModeratedCaucus: boolean().notNull().default(false), + resolutionFeatureEnabled: boolean().notNull().default(true) }); export const committeeStatus = pgEnum('committee_status', [ @@ -72,7 +75,16 @@ export const committee = pgTable( customSimpleMajority: smallint(), // 50% by default customTwoThirdsMajority: smallint(), // 66% by default customPaperSupportThreshold: smallint(), // 10% by default - lastResolutionAdoptionDate: timestamp({ mode: 'date' }) + lastResolutionAdoptionDate: timestamp({ mode: 'date' }), + maxDraftResolutions: smallint().notNull().default(3), + activeDraftResolutionId: text().references((): AnyPgColumn => resolutionPaper.id), + currentOperativeIndex: smallint(), + currentOperativeClauseId: text(), + supportReEvaluationOpen: boolean().notNull().default(false), + amendmentSubmissionOpen: boolean().notNull().default(true), + amendmentSponsoringOpen: boolean().notNull().default(true), + activeAmendmentId: text().references((): AnyPgColumn => amendment.id, { onDelete: 'set null' }), + resolutionHeadline: text() }, (t) => [unique().on(t.conferenceId, t.name), unique().on(t.conferenceId, t.abbreviation)] ); @@ -224,3 +236,195 @@ export const presenceChangedTimestamp = pgTable('presence_changed_timestamp', { timestamp: timestamp().notNull(), presentSetTo: boolean().notNull() }); + +// Resolution enums + +export const paperStatus = pgEnum('paper_status', [ + 'WORKING_PAPER', + 'SUBMITTED', + 'DRAFT_RESOLUTION', + 'AMENDMENT_PHASE', + 'VOTING_PHASE', + 'FINAL' +]); + +export const shareCodePermission = pgEnum('share_code_permission', ['SPONSOR', 'EDIT']); + +export const commentVisibility = pgEnum('comment_visibility', ['PUBLIC', 'TEAM_ONLY']); + +export const amendmentType = pgEnum('amendment_type', [ + 'DELETE', + 'ADD', + 'ALTER_TEXT', + 'ALTER_POSITION' +]); + +export const amendmentStatus = pgEnum('amendment_status', [ + 'PENDING', + 'SUBMITTED', + 'CONSENSUS_ADOPTED', + 'ACCEPTED', + 'REJECTED', + 'WITHDRAWN' +]); + +export const voteOutcome = pgEnum('vote_outcome', ['ADOPTED', 'REJECTED', 'SENT_BACK']); + +// Resolution tables + +export const resolutionPaper = pgTable('resolution_paper', { + ...defaultIdAndTimestamps, + committeeId: text() + .notNull() + .references(() => committee.id, { onDelete: 'cascade' }), + agendaItemId: text() + .notNull() + .references(() => agendaItem.id, { onDelete: 'cascade' }), + creatorCommitteeMemberId: text() + .notNull() + .references(() => committeeMember.id, { onDelete: 'cascade' }), + status: paperStatus().notNull().default('WORKING_PAPER'), + content: json(), + title: text(), + documentNumber: text(), + sequenceNumber: smallint(), + deletedAt: timestamp() +}); + +export const paperContentSnapshot = pgTable('paper_content_snapshot', { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + content: json(), + trigger: text() +}); + +export const paperSponsor = pgTable( + 'paper_sponsor', + { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + committeeMemberId: text() + .notNull() + .references(() => committeeMember.id, { onDelete: 'cascade' }) + }, + (t) => [unique().on(t.paperId, t.committeeMemberId)] +); + +export const paperShareCode = pgTable('paper_share_code', { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + code: text().notNull().unique(), + permission: shareCodePermission().notNull() +}); + +export const paperEditor = pgTable( + 'paper_editor', + { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + conferenceUserId: text() + .notNull() + .references(() => conferenceUser.id, { onDelete: 'cascade' }) + }, + (t) => [unique().on(t.paperId, t.conferenceUserId)] +); + +export const resolutionComment = pgTable('resolution_comment', { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + clauseId: text(), + authorConferenceUserId: text() + .notNull() + .references(() => conferenceUser.id, { onDelete: 'cascade' }), + content: text().notNull(), + visibility: commentVisibility().notNull().default('PUBLIC'), + parentCommentId: text().references((): AnyPgColumn => resolutionComment.id, { + onDelete: 'cascade' + }) +}); + +export const amendment = pgTable('amendment', { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + proposerCommitteeMemberId: text() + .notNull() + .references(() => committeeMember.id, { onDelete: 'cascade' }), + type: amendmentType().notNull(), + status: amendmentStatus().notNull().default('PENDING'), + targetClauseId: text(), + targetOperativeIndex: smallint(), + newContent: json(), + targetPosition: smallint(), + documentNumber: text(), + sequenceNumber: smallint() +}); + +export const amendmentSponsor = pgTable( + 'amendment_sponsor', + { + ...defaultIdAndTimestamps, + amendmentId: text() + .notNull() + .references(() => amendment.id, { onDelete: 'cascade' }), + committeeMemberId: text() + .notNull() + .references(() => committeeMember.id, { onDelete: 'cascade' }) + }, + (t) => [unique().on(t.amendmentId, t.committeeMemberId)] +); + +export const operativeClauseVote = pgTable( + 'operative_clause_vote', + { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + clauseId: text().notNull(), + outcome: voteOutcome().notNull(), + votesFor: integer().notNull(), + votesAgainst: integer().notNull(), + votesAbstain: integer().notNull().default(0) + }, + (t) => [unique().on(t.paperId, t.clauseId)] +); + +export const paperClauseLock = pgTable( + 'paper_clause_lock', + { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + clauseId: text().notNull(), + conferenceUserId: text() + .notNull() + .references(() => conferenceUser.id, { onDelete: 'cascade' }), + acquiredAt: timestamp({ mode: 'date' }).defaultNow().notNull() + }, + (t) => [unique().on(t.paperId, t.clauseId)] +); + +export const resolutionVoteResult = pgTable('resolution_vote_result', { + ...defaultIdAndTimestamps, + paperId: text() + .notNull() + .unique() + .references(() => resolutionPaper.id, { onDelete: 'cascade' }), + outcome: voteOutcome().notNull(), + votesFor: integer().notNull(), + votesAgainst: integer().notNull(), + votesAbstain: integer().notNull().default(0) +}); diff --git a/src/api/handlers/agendaItem.ts b/src/api/handlers/agendaItem.ts index 47588e6c..bf964e91 100644 --- a/src/api/handlers/agendaItem.ts +++ b/src/api/handlers/agendaItem.ts @@ -7,7 +7,7 @@ import { schemaBuilder, arg as rumbleArg } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { nanoid } from '$lib/helpers/nanoid'; import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; import { GraphQLError } from 'graphql'; @@ -36,11 +36,13 @@ query({ table: 'agendaItem' }); -abilityBuilder.agendaItem.allow(['read']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.agendaItem.allow(['read']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.agendaItem.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); schemaBuilder.mutationFields((t) => { @@ -52,7 +54,7 @@ schemaBuilder.mutationFields((t) => { committeeId: t.arg({ type: 'ID', required: true }) }, resolve: async (query, root, args, ctx, info) => { - if (!ctx.hasRole('admin')) { + if (!isGlobalAdmin(ctx)) { // TODO: rumble should support something like this await db.query.conferenceUser .findFirst({ diff --git a/src/api/handlers/amendment.ts b/src/api/handlers/amendment.ts new file mode 100644 index 00000000..9a2435be --- /dev/null +++ b/src/api/handlers/amendment.ts @@ -0,0 +1,879 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { and, eq, count as drizzleCount, not, inArray, gt, gte, sql } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; +import { + ResolutionSchema, + OperativeClauseSchema +} from '@deutschemodelunitednations/munify-resolution-editor/schema'; + +const { arg, ref, pubsub, table } = basics('amendment'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +const amendmentTypeEnum = enum_({ tsName: 'amendmentType' }); +const amendmentStatusEnum = enum_({ tsName: 'amendmentStatus' }); + +abilityBuilder.amendment.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.amendment.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +// ============================================================================= +// HELPERS +// ============================================================================= + +function validateAmendmentArgs( + type: string, + args: { + targetClauseId?: string | null; + targetOperativeIndex?: number | null; + targetPosition?: number | null; + newContent?: unknown; + } +) { + switch (type) { + case 'DELETE': + if (args.targetClauseId === undefined || args.targetClauseId === null) { + throw new GraphQLError('DELETE amendments require targetClauseId'); + } + if (args.targetOperativeIndex === undefined || args.targetOperativeIndex === null) { + throw new GraphQLError('DELETE amendments require targetOperativeIndex'); + } + break; + case 'ADD': + if (args.targetPosition === undefined || args.targetPosition === null) { + throw new GraphQLError('ADD amendments require targetPosition'); + } + if (!args.newContent) { + throw new GraphQLError('ADD amendments require newContent'); + } + break; + case 'ALTER_TEXT': + if (args.targetClauseId === undefined || args.targetClauseId === null) { + throw new GraphQLError('ALTER_TEXT amendments require targetClauseId'); + } + if (args.targetOperativeIndex === undefined || args.targetOperativeIndex === null) { + throw new GraphQLError('ALTER_TEXT amendments require targetOperativeIndex'); + } + if (!args.newContent) { + throw new GraphQLError('ALTER_TEXT amendments require newContent'); + } + break; + case 'ALTER_POSITION': + if (args.targetOperativeIndex === undefined || args.targetOperativeIndex === null) { + throw new GraphQLError('ALTER_POSITION amendments require targetOperativeIndex (source)'); + } + if (args.targetPosition === undefined || args.targetPosition === null) { + throw new GraphQLError('ALTER_POSITION amendments require targetPosition (destination)'); + } + break; + } +} + +type Resolution = { committeeName: string; preamble: unknown[]; operative: unknown[] }; + +/** + * Find the current index of a clause by its stable ID. + * Throws if the clause is not found (e.g. already deleted by a prior amendment). + */ +function findClauseIndex(operative: { id: string }[], clauseId: string): number { + const idx = operative.findIndex((c) => c.id === clauseId); + if (idx === -1) { + throw new GraphQLError( + `Clause "${clauseId}" not found in resolution — it may have been deleted by a prior amendment` + ); + } + return idx; +} + +/** + * Auto-adjust targetPosition on remaining PENDING/SUBMITTED ADD/ALTER_POSITION amendments + * after a structural change (deletion or insertion) shifts operative clause indices. + */ +async function adjustPendingPositions( + tx: Parameters[0]>[0], + paperId: string, + excludeAmendmentId: string, + direction: 'decrement' | 'increment', + thresholdIndex: number, + comparison: 'gt' | 'gte' +) { + const delta = direction === 'decrement' ? -1 : 1; + const cmp = + comparison === 'gt' + ? gt(schema.amendment.targetPosition, thresholdIndex) + : gte(schema.amendment.targetPosition, thresholdIndex); + + await tx + .update(schema.amendment) + .set({ targetPosition: sql`${schema.amendment.targetPosition} + ${delta}` }) + .where( + and( + eq(schema.amendment.paperId, paperId), + inArray(schema.amendment.status, ['PENDING', 'SUBMITTED']), + inArray(schema.amendment.type, ['ADD', 'ALTER_POSITION']), + not(eq(schema.amendment.id, excludeAmendmentId)), + cmp + ) + ); +} + +async function applyAmendmentToResolution( + tx: Parameters[0]>[0], + amendment: typeof schema.amendment.$inferSelect, + paper: typeof schema.resolutionPaper.$inferSelect +) { + const parsed = ResolutionSchema.safeParse(paper.content); + if (!parsed.success) { + throw new GraphQLError('Paper content is invalid'); + } + const resolution = parsed.data; + + // Snapshot before applying + await tx.insert(schema.paperContentSnapshot).values({ + paperId: paper.id, + content: paper.content, + trigger: `AMENDMENT_${amendment.type}` + }); + + switch (amendment.type) { + case 'DELETE': { + // Resolve current index from stable clause ID (not stored index) + const idx = findClauseIndex(resolution.operative, amendment.targetClauseId!); + resolution.operative.splice(idx, 1); + // Auto-withdraw other PENDING/SUBMITTED amendments targeting the deleted clause + await tx + .update(schema.amendment) + .set({ status: 'WITHDRAWN' }) + .where( + and( + eq(schema.amendment.paperId, paper.id), + eq(schema.amendment.targetClauseId, amendment.targetClauseId!), + inArray(schema.amendment.status, ['PENDING', 'SUBMITTED']), + not(eq(schema.amendment.id, amendment.id)) + ) + ); + // Adjust targetPosition on remaining ADD/ALTER_POSITION amendments + await adjustPendingPositions(tx, paper.id, amendment.id, 'decrement', idx, 'gt'); + break; + } + case 'ADD': { + const parsedClause = OperativeClauseSchema.safeParse(amendment.newContent); + if (!parsedClause.success) { + throw new GraphQLError('Invalid newContent for ADD amendment'); + } + const insertAfter = amendment.targetPosition!; + resolution.operative.splice(insertAfter + 1, 0, parsedClause.data); + // Adjust targetPosition on remaining ADD/ALTER_POSITION amendments + await adjustPendingPositions(tx, paper.id, amendment.id, 'increment', insertAfter, 'gte'); + break; + } + case 'ALTER_TEXT': { + // Resolve current index from stable clause ID + const idx = findClauseIndex(resolution.operative, amendment.targetClauseId!); + const parsedClause = OperativeClauseSchema.safeParse(amendment.newContent); + if (!parsedClause.success) { + throw new GraphQLError('Invalid newContent for ALTER_TEXT amendment'); + } + // Keep original clause ID, replace blocks + resolution.operative[idx] = { + ...parsedClause.data, + id: resolution.operative[idx].id + }; + break; + } + case 'ALTER_POSITION': { + // Resolve current index from stable clause ID + const sourceIdx = findClauseIndex(resolution.operative, amendment.targetClauseId!); + const destIdx = amendment.targetPosition!; + if (destIdx < 0 || destIdx > resolution.operative.length) { + throw new GraphQLError('Destination index out of range'); + } + const [clause] = resolution.operative.splice(sourceIdx, 1); + // After removing from source, the target index might shift + const adjustedDest = destIdx > sourceIdx ? destIdx - 1 : destIdx; + resolution.operative.splice(adjustedDest, 0, clause); + // Adjust other pending amendments' targetPosition for the structural shift + // First: source removal shifts indices down + await adjustPendingPositions(tx, paper.id, amendment.id, 'decrement', sourceIdx, 'gt'); + // Then: destination insertion shifts indices up + await adjustPendingPositions(tx, paper.id, amendment.id, 'increment', adjustedDest, 'gte'); + break; + } + } + + await tx + .update(schema.resolutionPaper) + .set({ content: resolution }) + .where(eq(schema.resolutionPaper.id, paper.id)); +} + +// ============================================================================= +// MUTATIONS +// ============================================================================= + +schemaBuilder.mutationFields((t) => ({ + createAmendment: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + type: t.arg({ type: amendmentTypeEnum, required: true }), + targetClauseId: t.arg.string(), + targetOperativeIndex: t.arg.int(), + targetPosition: t.arg.int(), + newContent: t.arg({ type: 'JSON' }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Find delegate's committee member + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + if (paper.status !== 'AMENDMENT_PHASE') { + throw new GraphQLError('Paper must be in AMENDMENT_PHASE'); + } + + // Verify this is the active DR + const committee = await db.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + + if (committee.activeDraftResolutionId !== paper.id) { + throw new GraphQLError('Paper must be the active draft resolution'); + } + + if (!committee.amendmentSubmissionOpen) { + throw new GraphQLError('Amendment submission is currently closed'); + } + + // Find the delegate's conference user + committee member + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + conferenceUserType: 'DELEGATE', + committeeMember: { + committeeId: paper.committeeId + } + } + }) + .then(assertFindFirstExists); + + if (!conferenceUser.committeeMemberId) { + throw new GraphQLError('You must be assigned to a committee member'); + } + + // Validate type-specific args + validateAmendmentArgs(args.type, args); + + // If targetClauseId is provided, resolve and auto-correct the operative index + if (args.targetClauseId) { + const parsed = ResolutionSchema.safeParse(paper.content); + if (parsed.success) { + const actualIdx = parsed.data.operative.findIndex((c) => c.id === args.targetClauseId); + if (actualIdx === -1) { + throw new GraphQLError('Target clause no longer exists in the resolution'); + } + // Auto-correct stale index from client + if (args.targetOperativeIndex !== actualIdx) { + args.targetOperativeIndex = actualIdx; + } + } + } + + // For DELETE and ALTER_TEXT, validate targetOperativeIndex >= currentOperativeIndex + if ( + (args.type === 'DELETE' || args.type === 'ALTER_TEXT') && + committee.currentOperativeIndex !== null && + args.targetOperativeIndex !== undefined && + args.targetOperativeIndex !== null && + args.targetOperativeIndex < committee.currentOperativeIndex + ) { + throw new GraphQLError('Cannot amend a clause that has already been passed'); + } + + // Validate newContent if provided + if (args.newContent) { + const parsedContent = OperativeClauseSchema.safeParse(args.newContent); + if (!parsedContent.success) { + throw new GraphQLError('Invalid newContent: ' + parsedContent.error.message); + } + } + + // Check for duplicate amendment (same proposer, type, and target clause) + { + const duplicateConditions = [ + eq(schema.amendment.paperId, args.paperId), + eq(schema.amendment.proposerCommitteeMemberId, conferenceUser.committeeMemberId), + eq(schema.amendment.type, args.type), + inArray(schema.amendment.status, ['PENDING', 'SUBMITTED']) + ]; + + // Use targetClauseId for duplicate detection (stable, not affected by index drift) + if (args.targetClauseId) { + duplicateConditions.push(eq(schema.amendment.targetClauseId, args.targetClauseId)); + } + + const [{ count: duplicateCount }] = await db + .select({ count: drizzleCount() }) + .from(schema.amendment) + .where(and(...duplicateConditions)); + + if (Number(duplicateCount) > 0) { + throw new GraphQLError( + 'You have already submitted an amendment of this type for this clause' + ); + } + } + + // Count existing amendments of same type for this paper to assign sequence number + const [{ count: sameTypeCount }] = await db + .select({ count: drizzleCount() }) + .from(schema.amendment) + .where( + and(eq(schema.amendment.paperId, args.paperId), eq(schema.amendment.type, args.type)) + ); + + const typeSeq = Number(sameTypeCount) + 1; + + const typePrefixMap: Record = { + DELETE: 'DEL', + ALTER_TEXT: 'ALT', + ADD: 'ADD', + ALTER_POSITION: 'POS' + }; + const typePrefix = typePrefixMap[args.type]; + + const documentNumber = `${paper.documentNumber}/${typePrefix}.${typeSeq}`; + + // Create amendment + const result = await db + .insert(schema.amendment) + .values({ + paperId: args.paperId, + proposerCommitteeMemberId: conferenceUser.committeeMemberId, + type: args.type, + status: 'SUBMITTED', + targetClauseId: args.targetClauseId ?? undefined, + targetOperativeIndex: args.targetOperativeIndex ?? undefined, + newContent: args.newContent ?? undefined, + targetPosition: args.targetPosition ?? undefined, + documentNumber, + sequenceNumber: typeSeq + }) + .returning() + .then(assertFirstEntryExists); + + // Auto-add proposer as first sponsor + await db.insert(schema.amendmentSponsor).values({ + amendmentId: result.id, + committeeMemberId: conferenceUser.committeeMemberId + }); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: result.id } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + chairCreateAmendment: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + type: t.arg({ type: amendmentTypeEnum, required: true }), + committeeMemberId: t.arg.id({ required: true }), + targetClauseId: t.arg.string(), + targetOperativeIndex: t.arg.int(), + targetPosition: t.arg.int(), + newContent: t.arg({ type: 'JSON' }) + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + if (paper.status !== 'AMENDMENT_PHASE') { + throw new GraphQLError('Paper must be in AMENDMENT_PHASE'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + // Verify this is the active DR + const committee = await db.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + + if (committee.activeDraftResolutionId !== paper.id) { + throw new GraphQLError('Paper must be the active draft resolution'); + } + + // Validate committeeMemberId belongs to this committee + await db.query.committeeMember + .findFirst({ + where: { id: args.committeeMemberId, committeeId: paper.committeeId } + }) + .then(assertFindFirstExists); + + // Validate type-specific args + validateAmendmentArgs(args.type, args); + + // If targetClauseId is provided, resolve and auto-correct the operative index + if (args.targetClauseId) { + const parsed = ResolutionSchema.safeParse(paper.content); + if (parsed.success) { + const actualIdx = parsed.data.operative.findIndex((c) => c.id === args.targetClauseId); + if (actualIdx === -1) { + throw new GraphQLError('Target clause no longer exists in the resolution'); + } + // Auto-correct stale index from client + if (args.targetOperativeIndex !== actualIdx) { + args.targetOperativeIndex = actualIdx; + } + } + } + + // Validate newContent if provided + if (args.newContent) { + const parsedContent = OperativeClauseSchema.safeParse(args.newContent); + if (!parsedContent.success) { + throw new GraphQLError('Invalid newContent: ' + parsedContent.error.message); + } + } + + // Check for duplicate amendment (same proposer, type, and target clause) + { + const duplicateConditions = [ + eq(schema.amendment.paperId, args.paperId), + eq(schema.amendment.proposerCommitteeMemberId, args.committeeMemberId), + eq(schema.amendment.type, args.type), + inArray(schema.amendment.status, ['PENDING', 'SUBMITTED']) + ]; + + // Use targetClauseId for duplicate detection (stable, not affected by index drift) + if (args.targetClauseId) { + duplicateConditions.push(eq(schema.amendment.targetClauseId, args.targetClauseId)); + } + + const [{ count: duplicateCount }] = await db + .select({ count: drizzleCount() }) + .from(schema.amendment) + .where(and(...duplicateConditions)); + + if (Number(duplicateCount) > 0) { + throw new GraphQLError( + 'You have already submitted an amendment of this type for this clause' + ); + } + } + + // Count existing amendments of same type for this paper to assign sequence number + const [{ count: sameTypeCount }] = await db + .select({ count: drizzleCount() }) + .from(schema.amendment) + .where( + and(eq(schema.amendment.paperId, args.paperId), eq(schema.amendment.type, args.type)) + ); + + const typeSeq = Number(sameTypeCount) + 1; + + const typePrefixMap: Record = { + DELETE: 'DEL', + ALTER_TEXT: 'ALT', + ADD: 'ADD', + ALTER_POSITION: 'POS' + }; + const typePrefix = typePrefixMap[args.type]; + + const documentNumber = `${paper.documentNumber}/${typePrefix}.${typeSeq}`; + + // Create amendment + const result = await db + .insert(schema.amendment) + .values({ + paperId: args.paperId, + proposerCommitteeMemberId: args.committeeMemberId, + type: args.type, + status: 'SUBMITTED', + targetClauseId: args.targetClauseId ?? undefined, + targetOperativeIndex: args.targetOperativeIndex ?? undefined, + newContent: args.newContent ?? undefined, + targetPosition: args.targetPosition ?? undefined, + documentNumber, + sequenceNumber: typeSeq + }) + .returning() + .then(assertFirstEntryExists); + + // Auto-add proposer as first sponsor + await db.insert(schema.amendmentSponsor).values({ + amendmentId: result.id, + committeeMemberId: args.committeeMemberId + }); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: result.id } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + adoptByConsensus: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Only SUBMITTED amendments can be adopted by consensus'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db.transaction(async (tx) => { + await tx + .update(schema.amendment) + .set({ status: 'CONSENSUS_ADOPTED' }) + .where(eq(schema.amendment.id, args.amendmentId)); + + await applyAmendmentToResolution(tx, amendment, paper); + }); + + pubsub.updated(args.amendmentId); + paperPubsub.updated(amendment.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + acceptAmendment: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Only SUBMITTED amendments can be accepted'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db.transaction(async (tx) => { + await tx + .update(schema.amendment) + .set({ status: 'ACCEPTED' }) + .where(eq(schema.amendment.id, args.amendmentId)); + + await applyAmendmentToResolution(tx, amendment, paper); + }); + + pubsub.updated(args.amendmentId); + paperPubsub.updated(amendment.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + rejectAmendment: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Only SUBMITTED amendments can be rejected'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db + .update(schema.amendment) + .set({ status: 'REJECTED' }) + .where(eq(schema.amendment.id, args.amendmentId)); + + pubsub.updated(args.amendmentId); + paperPubsub.updated(amendment.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + withdrawAmendment: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + ctx.mustBeLoggedIn(); + + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Only SUBMITTED amendments can be withdrawn'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db + .update(schema.amendment) + .set({ status: 'WITHDRAWN' }) + .where(eq(schema.amendment.id, args.amendmentId)); + + pubsub.updated(args.amendmentId); + paperPubsub.updated(amendment.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + editAmendment: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }), + targetClauseId: t.arg.string(), + targetOperativeIndex: t.arg.int(), + targetPosition: t.arg.int(), + newContent: t.arg({ type: 'JSON' }), + proposerCommitteeMemberId: t.arg.id() + }, + resolve: async (query, root, args, ctx, info) => { + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Only SUBMITTED amendments can be edited'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + // Merge provided args with existing values + const merged = { + targetClauseId: + args.targetClauseId !== undefined ? args.targetClauseId : amendment.targetClauseId, + targetOperativeIndex: + args.targetOperativeIndex !== undefined + ? args.targetOperativeIndex + : amendment.targetOperativeIndex, + targetPosition: + args.targetPosition !== undefined ? args.targetPosition : amendment.targetPosition, + newContent: args.newContent !== undefined ? args.newContent : amendment.newContent + }; + + // Re-validate with merged values + validateAmendmentArgs(amendment.type, merged); + + // If targetClauseId is provided, resolve and auto-correct the operative index + if (merged.targetClauseId) { + const parsed = ResolutionSchema.safeParse(paper.content); + if (parsed.success) { + const actualIdx = parsed.data.operative.findIndex((c) => c.id === merged.targetClauseId); + if (actualIdx === -1) { + throw new GraphQLError('Target clause no longer exists in the resolution'); + } + // Auto-correct stale index + if (merged.targetOperativeIndex !== actualIdx) { + merged.targetOperativeIndex = actualIdx; + args.targetOperativeIndex = actualIdx; + } + } + } + + // Validate newContent if provided + if (args.newContent) { + const parsedContent = OperativeClauseSchema.safeParse(args.newContent); + if (!parsedContent.success) { + throw new GraphQLError('Invalid newContent: ' + parsedContent.error.message); + } + } + + // Check if anything actually changed + const updateFields: Record = {}; + if (args.targetClauseId !== undefined && args.targetClauseId !== amendment.targetClauseId) { + updateFields.targetClauseId = args.targetClauseId; + } + if ( + args.targetOperativeIndex !== undefined && + args.targetOperativeIndex !== amendment.targetOperativeIndex + ) { + updateFields.targetOperativeIndex = args.targetOperativeIndex; + } + if (args.targetPosition !== undefined && args.targetPosition !== amendment.targetPosition) { + updateFields.targetPosition = args.targetPosition; + } + if (args.newContent !== undefined) { + updateFields.newContent = args.newContent; + } + + const proposerChanged = + args.proposerCommitteeMemberId !== undefined && + args.proposerCommitteeMemberId !== null && + args.proposerCommitteeMemberId !== amendment.proposerCommitteeMemberId; + + if (Object.keys(updateFields).length === 0 && !proposerChanged) { + // Nothing changed + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + + if (proposerChanged) { + // Validate new proposer belongs to committee + await db.query.committeeMember + .findFirst({ + where: { id: args.proposerCommitteeMemberId!, committeeId: paper.committeeId } + }) + .then(assertFindFirstExists); + + updateFields.proposerCommitteeMemberId = args.proposerCommitteeMemberId; + } + + await db.transaction(async (tx) => { + if (proposerChanged) { + const oldProposerId = amendment.proposerCommitteeMemberId; + const newProposerId = args.proposerCommitteeMemberId!; + + // Remove old proposer's sponsor entry + await tx + .delete(schema.amendmentSponsor) + .where( + and( + eq(schema.amendmentSponsor.amendmentId, args.amendmentId), + eq(schema.amendmentSponsor.committeeMemberId, oldProposerId) + ) + ); + + // Add new proposer as sponsor if not already one + const existingSponsor = await tx.query.amendmentSponsor.findFirst({ + where: { + amendmentId: args.amendmentId, + committeeMemberId: newProposerId + } + }); + if (!existingSponsor) { + await tx.insert(schema.amendmentSponsor).values({ + amendmentId: args.amendmentId, + committeeMemberId: newProposerId + }); + } + } + + await tx + .update(schema.amendment) + .set(updateFields) + .where(eq(schema.amendment.id, args.amendmentId)); + }); + + pubsub.updated(args.amendmentId); + paperPubsub.updated(amendment.paperId); + + return db.query.amendment + .findFirst( + query( + ctx.abilities.amendment.filter('read', { + inject: { where: { id: args.amendmentId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }) +})); diff --git a/src/api/handlers/amendmentSponsor.ts b/src/api/handlers/amendmentSponsor.ts new file mode 100644 index 00000000..dbe74939 --- /dev/null +++ b/src/api/handlers/amendmentSponsor.ts @@ -0,0 +1,157 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { and, eq } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; + +const { arg, ref, pubsub, table } = basics('amendmentSponsor'); +const amendmentPubsub = rumblePubsub({ table: 'amendment' }); + +abilityBuilder.amendmentSponsor.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.amendmentSponsor.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + addAmendmentSponsor: t.drizzleField({ + type: ref, + args: { + amendmentId: t.arg.id({ required: true }), + committeeMemberId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Can only add sponsors to SUBMITTED amendments'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + // Either the delegate adding themselves, or chair adding anyone + const isOwnMembership = await db.query.conferenceUser.findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: args.committeeMemberId + } + }); + + if (!isOwnMembership) { + // Must be chair/admin + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + } else { + // Delegate adding themselves — check if sponsoring is open + const committee = await db.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + + if (!committee.amendmentSponsoringOpen) { + throw new GraphQLError('Amendment sponsoring is currently closed'); + } + } + + // Check not already sponsor + const existing = await db.query.amendmentSponsor.findFirst({ + where: { + amendmentId: args.amendmentId, + committeeMemberId: args.committeeMemberId + } + }); + + if (existing) { + throw new GraphQLError('Already a sponsor of this amendment'); + } + + const result = await db + .insert(schema.amendmentSponsor) + .values({ + amendmentId: args.amendmentId, + committeeMemberId: args.committeeMemberId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.created(); + amendmentPubsub.updated(args.amendmentId); + + return db.query.amendmentSponsor + .findFirst( + query( + ctx.abilities.amendmentSponsor.filter('read', { + inject: { where: { id: result.id } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + removeAmendmentSponsor: t.field({ + type: 'Boolean', + args: { + amendmentId: t.arg.id({ required: true }), + committeeMemberId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx) => { + ctx.mustBeLoggedIn(); + + const amendment = await db.query.amendment + .findFirst({ where: { id: args.amendmentId } }) + .then(assertFindFirstExists); + + if (amendment.status !== 'SUBMITTED') { + throw new GraphQLError('Can only remove sponsors from SUBMITTED amendments'); + } + + // Cannot remove the proposer + if (args.committeeMemberId === amendment.proposerCommitteeMemberId) { + throw new GraphQLError('Cannot remove the proposer from sponsors'); + } + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: amendment.paperId } }) + .then(assertFindFirstExists); + + // Only chair/admin can remove sponsors + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + const existing = await db.query.amendmentSponsor.findFirst({ + where: { + amendmentId: args.amendmentId, + committeeMemberId: args.committeeMemberId + } + }); + + if (!existing) { + throw new GraphQLError('Not a sponsor of this amendment'); + } + + await db + .delete(schema.amendmentSponsor) + .where( + and( + eq(schema.amendmentSponsor.amendmentId, args.amendmentId), + eq(schema.amendmentSponsor.committeeMemberId, args.committeeMemberId) + ) + ); + + pubsub.removed(existing.id); + amendmentPubsub.updated(args.amendmentId); + + return true; + } + }) +})); diff --git a/src/api/handlers/committee.ts b/src/api/handlers/committee.ts index d58002b4..a32bd780 100644 --- a/src/api/handlers/committee.ts +++ b/src/api/handlers/committee.ts @@ -8,20 +8,27 @@ import { schemaBuilder, arg as rumbleArg } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; -import { assertFirstEntryExists } from '@m1212e/rumble'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; import { and, count, eq, type InferSelectModel } from 'drizzle-orm'; import { calculateMajority } from '$lib/utils/majorities'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; +import { GraphQLError } from 'graphql'; + +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); const statusEnum = enum_({ tsName: 'committeeStatus' }); -abilityBuilder.committee.allow(['read', 'update']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.committee.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.committee.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); const getTotalPresentCount = async ( @@ -104,13 +111,72 @@ query({ schemaBuilder.mutationFields((t) => { return { + createCommittee: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + name: t.arg.string({ required: true }), + abbreviation: t.arg.string({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.committee) + .values({ + conferenceId: args.conferenceId, + name: args.name, + abbreviation: args.abbreviation + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.committee + .findFirst( + query( + ctx.abilities.committee.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteCommittee: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const committee = await db.query.committee.findFirst({ + where: { id: args.id } + }); + + if (!committee) { + throw new GraphQLError('Committee not found'); + } + + await assertConferenceAdmin(ctx, committee.conferenceId); + + await db.delete(schema.committee).where(eq(schema.committee.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }), + updateCommittee: t.drizzleField({ type: ref, args: { id: t.arg.id({ required: true }), - //TODO do we want to allow updates to these defaults? - // e.g. abbreviation and name probably are pretty static... - // name: t.arg.string(), + name: t.arg.string(), + abbreviation: t.arg.string(), whiteboardContent: t.arg.string(), showWhiteboard: t.arg.boolean(), status: t.arg({ @@ -124,12 +190,53 @@ schemaBuilder.mutationFields((t) => { activeAgendaItemId: t.arg.id(), lastResolutionAdoptionDate: t.arg({ type: 'DateTime' - }) + }), + allowDelegationsToAddThemselvesToSpeakersList: t.arg.boolean(), + maxDraftResolutions: t.arg.int(), + activeDraftResolutionId: t.arg.id(), + clearActiveDraftResolution: t.arg.boolean(), + currentOperativeIndex: t.arg.int(), + currentOperativeClauseId: t.arg.string(), + supportReEvaluationOpen: t.arg.boolean(), + amendmentSubmissionOpen: t.arg.boolean(), + amendmentSponsoringOpen: t.arg.boolean(), + activeAmendmentId: t.arg.id(), + clearActiveAmendment: t.arg.boolean() }, resolve: async (query, root, args, ctx, info) => { + await assertCommitteeChairOrAdmin(ctx, args.id); + + // Validate activeDraftResolutionId if provided + if (args.activeDraftResolutionId) { + const paper = await db.query.resolutionPaper.findFirst({ + where: { id: args.activeDraftResolutionId } + }); + + if (!paper) { + throw new GraphQLError('Paper not found'); + } + if (paper.committeeId !== args.id) { + throw new GraphQLError('Paper does not belong to this committee'); + } + if ( + paper.status !== 'DRAFT_RESOLUTION' && + paper.status !== 'AMENDMENT_PHASE' && + paper.status !== 'VOTING_PHASE' + ) { + throw new GraphQLError('Only draft resolutions can be set as active'); + } + } + + // Auto-close re-evaluation when setting an active DR + const supportReEvaluationOpen = args.activeDraftResolutionId + ? false + : (args.supportReEvaluationOpen ?? undefined); + await db .update(schema.committee) .set({ + name: args.name ?? undefined, + abbreviation: args.abbreviation ?? undefined, whiteboardContent: args.whiteboardContent ?? undefined, showWhiteboard: args.showWhiteboard ?? undefined, status: args.status ?? undefined, @@ -137,14 +244,54 @@ schemaBuilder.mutationFields((t) => { statusUntil: args.statusUntil ?? undefined, stateOfDebate: args.stateOfDebate ?? undefined, activeAgendaItemId: args.activeAgendaItemId ?? undefined, - lastResolutionAdoptionDate: args.lastResolutionAdoptionDate ?? undefined + lastResolutionAdoptionDate: args.lastResolutionAdoptionDate ?? undefined, + allowDelegationsToAddThemselvesToSpeakersList: + args.allowDelegationsToAddThemselvesToSpeakersList ?? undefined, + maxDraftResolutions: args.maxDraftResolutions ?? undefined, + activeDraftResolutionId: args.clearActiveDraftResolution + ? null + : (args.activeDraftResolutionId ?? undefined), + currentOperativeIndex: args.currentOperativeIndex ?? undefined, + currentOperativeClauseId: args.currentOperativeClauseId ?? undefined, + supportReEvaluationOpen, + amendmentSubmissionOpen: args.amendmentSubmissionOpen ?? undefined, + amendmentSponsoringOpen: args.amendmentSponsoringOpen ?? undefined, + activeAmendmentId: args.clearActiveAmendment + ? null + : (args.activeAmendmentId ?? undefined) }) - .where( - and( - eq(schema.committee.id, args.id), - ctx.abilities.committee.filter('update').sql.where - ) - ); + .where(eq(schema.committee.id, args.id)); + + // Auto-transition active DR to AMENDMENT_PHASE when currentOperativeIndex is set + if (args.currentOperativeIndex !== undefined && args.currentOperativeIndex !== null) { + const committee = await db.query.committee.findFirst({ + where: { id: args.id } + }); + + const activeDrId = args.activeDraftResolutionId ?? committee?.activeDraftResolutionId; + + if (activeDrId) { + const activeDr = await db.query.resolutionPaper.findFirst({ + where: { id: activeDrId } + }); + + if (activeDr && activeDr.status === 'DRAFT_RESOLUTION') { + await db + .update(schema.resolutionPaper) + .set({ status: 'AMENDMENT_PHASE' }) + .where(eq(schema.resolutionPaper.id, activeDrId)); + + // Create snapshot + await db.insert(schema.paperContentSnapshot).values({ + paperId: activeDrId, + content: activeDr.content, + trigger: 'AMENDMENT_PHASE' + }); + + paperPubsub.updated(activeDrId); + } + } + } if (args.activeAgendaItemId) { await db.insert(schema.committeeTopicChangedTimestamp).values({ diff --git a/src/api/handlers/committeeMember.ts b/src/api/handlers/committeeMember.ts index aaf92f6b..b4df6206 100644 --- a/src/api/handlers/committeeMember.ts +++ b/src/api/handlers/committeeMember.ts @@ -1,20 +1,93 @@ import { db, schema } from '$api/db/db'; import { abilityBuilder, schemaBuilder } from '$api/rumble'; -import { and, inArray } from 'drizzle-orm'; +import { eq, inArray } from 'drizzle-orm'; import { basics } from './basics'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('committeeMember'); -abilityBuilder.committeeMember.allow(['read', 'update']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.committeeMember.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.committeeMember.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); schemaBuilder.mutationFields((t) => { return { + createCommitteeMember: t.drizzleField({ + type: ref, + args: { + committeeId: t.arg.id({ required: true }), + representationId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const committee = await db.query.committee.findFirst({ + where: { id: args.committeeId } + }); + + if (!committee) { + throw new GraphQLError('Committee not found'); + } + + await assertConferenceAdmin(ctx, committee.conferenceId); + + const result = await db + .insert(schema.committeeMember) + .values({ + committeeId: args.committeeId, + representationId: args.representationId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.committeeMember + .findFirst( + query( + ctx.abilities.committeeMember.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteCommitteeMember: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const committeeMember = await db.query.committeeMember.findFirst({ + where: { id: args.id }, + with: { committee: true } + }); + + if (!committeeMember) { + throw new GraphQLError('Committee member not found'); + } + + await assertConferenceAdmin(ctx, committeeMember.committee.conferenceId); + + await db.delete(schema.committeeMember).where(eq(schema.committeeMember.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }), + setPresenceForCommitteeMembers: t.drizzleField({ type: [ref], args: { @@ -22,28 +95,35 @@ schemaBuilder.mutationFields((t) => { present: t.arg.boolean({ required: true }) }, resolve: async (query, root, args, ctx, info) => { + // Look up committee for the given members and verify chair/admin access + const members = await db.query.committeeMember.findMany({ + where: { id: { in: args.ids } }, + columns: { committeeId: true } + }); + const committeeIds = [...new Set(members.map((m) => m.committeeId))]; + for (const committeeId of committeeIds) { + await assertCommitteeChairOrAdmin(ctx, committeeId); + } + const res = await db .update(table) .set({ present: args.present }) - .where( - and( - inArray(table.id, args.ids), - ctx.abilities.committeeMember.filter('update').sql.where - ) - ) + .where(inArray(table.id, args.ids)) .returning({ id: table.id }); - await db.insert(schema.presenceChangedTimestamp).values( - res.map((committeeMember) => ({ - committeeMemberId: committeeMember.id, - presentSetTo: args.present, - timestamp: new Date() - })) - ); + if (res.length > 0) { + await db.insert(schema.presenceChangedTimestamp).values( + res.map((committeeMember) => ({ + committeeMemberId: committeeMember.id, + presentSetTo: args.present, + timestamp: new Date() + })) + ); + } pubsub.updated(args.ids); diff --git a/src/api/handlers/conference.ts b/src/api/handlers/conference.ts index ba543472..924c0af8 100644 --- a/src/api/handlers/conference.ts +++ b/src/api/handlers/conference.ts @@ -1,26 +1,27 @@ -import { db } from '$api/db/db'; +import { db, schema } from '$api/db/db'; import { abilityBuilder, object, query, + schemaBuilder, pubsub as rumblePubsub, arg as rumbleArg } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { ConferenceMemberRef, ConferenceMemberWhereInput } from './conferenceMember'; +import { assertConferenceAdmin } from './conferenceUser'; +import { eq } from 'drizzle-orm'; +import { assertFindFirstExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; -abilityBuilder.conference.allow('read').when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); +abilityBuilder.conference.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.conference.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); -// .when(({ user }) => { -// if (user) { -// return {}; -// } -// }); const ref = object({ table: 'conference', @@ -62,10 +63,76 @@ const ref = object({ }) }); -const pubsub = rumblePubsub({ table: 'committee' }); -const arg = rumbleArg({ table: 'committee' }); +const pubsub = rumblePubsub({ table: 'conference' }); +const arg = rumbleArg({ table: 'conference' }); query({ table: 'conference' }); +schemaBuilder.mutationFields((t) => ({ + updateConference: t.drizzleField({ + type: ref, + args: { + id: t.arg.id({ required: true }), + title: t.arg.string(), + pressWebsite: t.arg.string(), + hasModeratedCaucus: t.arg.boolean(), + resolutionFeatureEnabled: t.arg.boolean() + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.id); + + await db + .update(schema.conference) + .set({ + title: args.title ?? undefined, + pressWebsite: args.pressWebsite ?? undefined, + hasModeratedCaucus: args.hasModeratedCaucus ?? undefined, + resolutionFeatureEnabled: args.resolutionFeatureEnabled ?? undefined + }) + .where(eq(schema.conference.id, args.id)); + + pubsub.updated(args.id); + + return db.query.conference + .findFirst( + query( + ctx.abilities.conference.filter('read', { + inject: { + where: { id: args.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }) +})); + +schemaBuilder.mutationFields((t) => ({ + deleteConference: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx) => { + if (!isGlobalAdmin(ctx)) { + throw new GraphQLError('Only global admins can delete conferences'); + } + + const conf = await db.query.conference.findFirst({ + where: { id: args.id } + }); + + if (!conf) { + throw new GraphQLError('Conference not found'); + } + + await db.delete(schema.conference).where(eq(schema.conference.id, args.id)); + + return true; + } + }) +})); + export const ConferenceRef = ref; diff --git a/src/api/handlers/conferenceMember.ts b/src/api/handlers/conferenceMember.ts index dec11436..93b67557 100644 --- a/src/api/handlers/conferenceMember.ts +++ b/src/api/handlers/conferenceMember.ts @@ -1,15 +1,82 @@ -import { abilityBuilder } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder } from '$api/rumble'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { basics } from './basics'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { eq } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('conferenceMember'); +abilityBuilder.conferenceMember.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + abilityBuilder.conferenceMember.allow('read').when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } + mustBeLoggedIn(); + return 'allow'; }); +schemaBuilder.mutationFields((t) => ({ + createConferenceMember: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + representationId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.conferenceMember) + .values({ + conferenceId: args.conferenceId, + representationId: args.representationId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.updated(result.id); + + return db.query.conferenceMember + .findFirst( + query( + ctx.abilities.conferenceMember.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteConferenceMember: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const conferenceMember = await db.query.conferenceMember.findFirst({ + where: { id: args.id } + }); + + if (!conferenceMember) { + throw new GraphQLError('Conference member not found'); + } + + await assertConferenceAdmin(ctx, conferenceMember.conferenceId); + + await db.delete(schema.conferenceMember).where(eq(schema.conferenceMember.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }) +})); + export const ConferenceMemberWhereInput = arg; export const ConferenceMemberRef = ref; diff --git a/src/api/handlers/conferenceUser.ts b/src/api/handlers/conferenceUser.ts index aef10446..520e8842 100644 --- a/src/api/handlers/conferenceUser.ts +++ b/src/api/handlers/conferenceUser.ts @@ -2,7 +2,7 @@ import { abilityBuilder, enum_, schemaBuilder } from '$api/rumble'; import { eq } from 'drizzle-orm'; import { basics } from './basics'; import { db, schema } from '$api/db/db'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { GraphQLError } from 'graphql'; import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; @@ -10,38 +10,25 @@ const { arg, ref, pubsub, table } = basics('conferenceUser'); export { ref as ConferenceUserRef }; -abilityBuilder.conferenceUser.allow('read').when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.conferenceUser.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; }); -// abilityBuilder.conferenceUser.allow('read').when(({ user }) => { -// if (user) { -// return { -// where: eq(schema.conferenceUser.id, user.sub) -// }; -// } -// }); - -// abilityBuilder.conferenceUser.allow('read').when(({ user }) => { -// // TODO -// if (user) { -// return {}; -// } -// }); +abilityBuilder.conferenceUser.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); /** * Helper to check if the current user is an ADMIN for a specific conference * (either OIDC admin or conference ADMIN role) */ -async function assertConferenceAdmin( +export async function assertConferenceAdmin( ctx: { hasRole: (role: string) => boolean; mustBeLoggedIn: () => { email?: string | null } }, conferenceId: string ) { - if (ctx.hasRole('admin')) { - return; // OIDC admin has access + if (isGlobalAdmin(ctx)) { + return; } const user = ctx.mustBeLoggedIn(); @@ -170,7 +157,9 @@ schemaBuilder.mutationFields((t) => ({ conferenceUserType: t.arg({ type: enum_({ tsName: 'conferenceUserType' }), required: true - }) + }), + committeeMemberId: t.arg({ type: 'ID' }), + conferenceMemberId: t.arg({ type: 'ID' }) }, resolve: async (query, root, args, ctx, info) => { // First get the conference user to find the conferenceId @@ -212,9 +201,52 @@ schemaBuilder.mutationFields((t) => ({ } } + // Validate committeeMemberId belongs to a committee in the same conference + if (args.committeeMemberId) { + const committeeMember = await db.query.committeeMember.findFirst({ + where: { id: args.committeeMemberId }, + with: { committee: true } + }); + if ( + !committeeMember || + committeeMember.committee.conferenceId !== conferenceUser.conferenceId + ) { + throw new GraphQLError('Committee member does not belong to this conference'); + } + } + + // Validate conferenceMemberId belongs to the same conference + if (args.conferenceMemberId) { + const conferenceMember = await db.query.conferenceMember.findFirst({ + where: { id: args.conferenceMemberId } + }); + if (!conferenceMember || conferenceMember.conferenceId !== conferenceUser.conferenceId) { + throw new GraphQLError('Conference member does not belong to this conference'); + } + } + + // Build the update set + const updateSet: Record = { + conferenceUserType: args.conferenceUserType + }; + + // Auto-clear: when role changes away from DELEGATE, clear committeeMemberId + if (args.conferenceUserType !== 'DELEGATE') { + updateSet.committeeMemberId = null; + } else if (args.committeeMemberId !== undefined) { + updateSet.committeeMemberId = args.committeeMemberId; + } + + // Auto-clear: when role changes away from NON_STATE_ACTOR, clear conferenceMemberId + if (args.conferenceUserType !== 'NON_STATE_ACTOR') { + updateSet.conferenceMemberId = null; + } else if (args.conferenceMemberId !== undefined) { + updateSet.conferenceMemberId = args.conferenceMemberId; + } + await db .update(schema.conferenceUser) - .set({ conferenceUserType: args.conferenceUserType }) + .set(updateSet) .where(eq(schema.conferenceUser.id, args.id)); pubsub.updated(args.id); diff --git a/src/api/handlers/import.ts b/src/api/handlers/import.ts index c6cbcb3c..f15199d4 100644 --- a/src/api/handlers/import.ts +++ b/src/api/handlers/import.ts @@ -3,6 +3,7 @@ import { enum_, schemaBuilder } from '$api/rumble'; import { importDataSchema } from '$lib/utils/import'; import { ConferenceRef } from './conference'; import { GraphQLError } from 'graphql'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; const Input = schemaBuilder.inputType('ImportData', { description: 'Import data. You can find the JSON schema here: /api/schema/import', @@ -15,7 +16,8 @@ const Input = schemaBuilder.inputType('ImportData', { fields: (t) => ({ id: t.id({ required: true }), name: t.string({ required: true }), - abbreviation: t.string({ required: true }) + abbreviation: t.string({ required: true }), + resolutionHeadline: t.string() }) }) ], @@ -107,8 +109,8 @@ schemaBuilder.mutationFields((t) => ({ }) }, resolve: async (query, root, args, ctx, info) => { - if (!ctx.hasRole('admin')) { - throw new GraphQLError('You must have the admin role!'); + if (!isGlobalAdmin(ctx)) { + throw new GraphQLError('You must be a global admin to create conferences!'); } // we want to ensure consistency between frontend and backend @@ -128,6 +130,7 @@ schemaBuilder.mutationFields((t) => ({ id: committee.id, name: committee.name, abbreviation: committee.abbreviation, + resolutionHeadline: committee.resolutionHeadline, conferenceId: data.id })) ); diff --git a/src/api/handlers/operativeClauseVote.ts b/src/api/handlers/operativeClauseVote.ts new file mode 100644 index 00000000..dbf226f3 --- /dev/null +++ b/src/api/handlers/operativeClauseVote.ts @@ -0,0 +1,116 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; +import { assertFindFirstExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; +import { eq, and } from 'drizzle-orm'; + +const { ref, pubsub } = basics('operativeClauseVote'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +abilityBuilder.operativeClauseVote.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.operativeClauseVote.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + recordClauseVote: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + clauseId: t.arg.string({ required: true }), + outcome: t.arg({ type: enum_({ tsName: 'voteOutcome' }), required: true }), + votesFor: t.arg.int({ required: true }), + votesAgainst: t.arg.int({ required: true }), + votesAbstain: t.arg.int() + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + if (paper.status !== 'VOTING_PHASE') { + throw new GraphQLError('Paper must be in VOTING_PHASE to record clause votes'); + } + + if (args.outcome === 'SENT_BACK') { + throw new GraphQLError('Clause votes can only be ADOPTED or REJECTED'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db + .insert(schema.operativeClauseVote) + .values({ + paperId: args.paperId, + clauseId: args.clauseId, + outcome: args.outcome, + votesFor: args.votesFor, + votesAgainst: args.votesAgainst, + votesAbstain: args.votesAbstain ?? 0 + }) + .onConflictDoUpdate({ + target: [schema.operativeClauseVote.paperId, schema.operativeClauseVote.clauseId], + set: { + outcome: args.outcome, + votesFor: args.votesFor, + votesAgainst: args.votesAgainst, + votesAbstain: args.votesAbstain ?? 0 + } + }); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.operativeClauseVote + .findFirst( + query({ + where: { + paperId: args.paperId, + clauseId: args.clauseId + } + }) + ) + .then(assertFindFirstExists); + } + }), + + deleteClauseVote: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }), + clauseId: t.arg.string({ required: true }) + }, + resolve: async (root, args, ctx) => { + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + if (paper.status !== 'VOTING_PHASE') { + throw new GraphQLError('Paper must be in VOTING_PHASE to delete clause votes'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db + .delete(schema.operativeClauseVote) + .where( + and( + eq(schema.operativeClauseVote.paperId, args.paperId), + eq(schema.operativeClauseVote.clauseId, args.clauseId) + ) + ); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return true; + } + }) +})); diff --git a/src/api/handlers/paperClauseLock.ts b/src/api/handlers/paperClauseLock.ts new file mode 100644 index 00000000..c5ec80d7 --- /dev/null +++ b/src/api/handlers/paperClauseLock.ts @@ -0,0 +1,191 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder } from '$api/rumble'; +import { and, eq, lt } from 'drizzle-orm'; +import { basics } from './basics'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; + +const { ref, pubsub } = basics('paperClauseLock'); + +const LOCK_EXPIRY_MS = 60_000; // 60 seconds + +abilityBuilder.paperClauseLock.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + acquireClauseLock: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + clauseId: t.arg.string({ required: true }) + }, + resolve: async (query, root, args, ctx) => { + const user = ctx.mustBeLoggedIn(); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub } + } + }) + .then(assertFindFirstExists); + + // Clean expired locks for this paper + const expiryThreshold = new Date(Date.now() - LOCK_EXPIRY_MS); + const expiredLocks = await db + .delete(schema.paperClauseLock) + .where( + and( + eq(schema.paperClauseLock.paperId, args.paperId), + lt(schema.paperClauseLock.acquiredAt, expiryThreshold) + ) + ) + .returning(); + + for (const expired of expiredLocks) { + pubsub.removed(expired.id); + } + + // Check existing lock for this (paperId, clauseId) + const existingLock = await db.query.paperClauseLock.findFirst({ + where: { + paperId: args.paperId, + clauseId: args.clauseId + } + }); + + if (existingLock) { + if (existingLock.conferenceUserId === conferenceUser.id) { + // Refresh own lock + await db + .update(schema.paperClauseLock) + .set({ acquiredAt: new Date() }) + .where(eq(schema.paperClauseLock.id, existingLock.id)); + + pubsub.updated(existingLock.id); + + return db.query.paperClauseLock + .findFirst( + query( + ctx.abilities.paperClauseLock.filter('read', { + inject: { where: { id: existingLock.id } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } else { + throw new GraphQLError('Clause is locked by another user'); + } + } + + // Insert new lock — unique constraint handles the race condition + try { + const result = await db + .insert(schema.paperClauseLock) + .values({ + paperId: args.paperId, + clauseId: args.clauseId, + conferenceUserId: conferenceUser.id + }) + .returning() + .then(assertFirstEntryExists); + + // Use created() not updated(id) — other clients' findMany subscriptions + // don't know this ID yet, so updated(id) wouldn't reach them + pubsub.created(); + return db.query.paperClauseLock + .findFirst( + query( + ctx.abilities.paperClauseLock.filter('read', { + inject: { where: { id: result.id } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } catch (e: unknown) { + // Unique constraint violation — another user won the race + if ( + e instanceof Error && + (e.message.includes('unique') || + e.message.includes('duplicate') || + e.message.includes('UNIQUE')) + ) { + throw new GraphQLError('Clause is locked by another user'); + } + throw e; + } + } + }), + + releaseClauseLock: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }), + clauseId: t.arg.string({ required: true }) + }, + resolve: async (root, args, ctx) => { + const user = ctx.mustBeLoggedIn(); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub } + } + }) + .then(assertFindFirstExists); + + const deleted = await db + .delete(schema.paperClauseLock) + .where( + and( + eq(schema.paperClauseLock.paperId, args.paperId), + eq(schema.paperClauseLock.clauseId, args.clauseId), + eq(schema.paperClauseLock.conferenceUserId, conferenceUser.id) + ) + ) + .returning(); + + for (const lock of deleted) { + pubsub.removed(lock.id); + } + + return true; + } + }), + + releaseAllMyLocks: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx) => { + const user = ctx.mustBeLoggedIn(); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub } + } + }) + .then(assertFindFirstExists); + + const deleted = await db + .delete(schema.paperClauseLock) + .where( + and( + eq(schema.paperClauseLock.paperId, args.paperId), + eq(schema.paperClauseLock.conferenceUserId, conferenceUser.id) + ) + ) + .returning(); + + for (const lock of deleted) { + pubsub.removed(lock.id); + } + + return true; + } + }) +})); diff --git a/src/api/handlers/paperContentSnapshot.ts b/src/api/handlers/paperContentSnapshot.ts new file mode 100644 index 00000000..5bb437e0 --- /dev/null +++ b/src/api/handlers/paperContentSnapshot.ts @@ -0,0 +1,14 @@ +import { abilityBuilder } from '$api/rumble'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; + +const { arg, ref, pubsub, table } = basics('paperContentSnapshot'); + +abilityBuilder.paperContentSnapshot.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.paperContentSnapshot.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); diff --git a/src/api/handlers/paperEditor.ts b/src/api/handlers/paperEditor.ts new file mode 100644 index 00000000..1fcb81df --- /dev/null +++ b/src/api/handlers/paperEditor.ts @@ -0,0 +1,71 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { and, eq } from 'drizzle-orm'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; + +const { arg, ref, pubsub, table } = basics('paperEditor'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +abilityBuilder.paperEditor.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.paperEditor.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + removeEditor: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }), + conferenceUserId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Must be paper creator + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: paper.creatorCommitteeMemberId + } + }) + .then(assertFindFirstExists); + + const editor = await db.query.paperEditor + .findFirst({ + where: { + paperId: args.paperId, + conferenceUserId: args.conferenceUserId + } + }) + .then(assertFindFirstExists); + + await db + .delete(schema.paperEditor) + .where( + and( + eq(schema.paperEditor.paperId, args.paperId), + eq(schema.paperEditor.conferenceUserId, args.conferenceUserId) + ) + ); + + pubsub.removed(editor.id); + paperPubsub.updated(args.paperId); + + return true; + } + }) +})); diff --git a/src/api/handlers/paperShareCode.ts b/src/api/handlers/paperShareCode.ts new file mode 100644 index 00000000..a73b6195 --- /dev/null +++ b/src/api/handlers/paperShareCode.ts @@ -0,0 +1,172 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { eq } from 'drizzle-orm'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; +import { customAlphabet } from 'nanoid'; + +const generateShareCode = customAlphabet('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', 6); + +const { arg, ref, pubsub, table } = basics('paperShareCode'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +const shareCodePermissionEnum = enum_({ tsName: 'shareCodePermission' }); + +abilityBuilder.paperShareCode.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.paperShareCode.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +const ShareCodeRedemptionResult = schemaBuilder + .objectRef<{ paperId: string; permission: string }>('ShareCodeRedemptionResult') + .implement({ + fields: (t) => ({ + paperId: t.exposeID('paperId'), + permission: t.exposeString('permission') + }) + }); + +schemaBuilder.mutationFields((t) => ({ + createShareCode: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + permission: t.arg({ type: shareCodePermissionEnum, required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Must be paper creator + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: paper.creatorCommitteeMemberId + } + }) + .then(assertFindFirstExists); + + const result = await db + .insert(schema.paperShareCode) + .values({ + paperId: args.paperId, + code: generateShareCode(), + permission: args.permission + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.paperShareCode + .findFirst( + query( + ctx.abilities.paperShareCode.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteShareCode: t.field({ + type: 'Boolean', + args: { + shareCodeId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const shareCode = await db.query.paperShareCode + .findFirst({ + where: { id: args.shareCodeId }, + with: { paper: true } + }) + .then(assertFindFirstExists); + + // Must be paper creator + await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: shareCode.paper.creatorCommitteeMemberId + } + }) + .then(assertFindFirstExists); + + await db.delete(schema.paperShareCode).where(eq(schema.paperShareCode.id, args.shareCodeId)); + + pubsub.removed(args.shareCodeId); + paperPubsub.updated(shareCode.paperId); + + return true; + } + }), + + redeemShareCode: t.field({ + type: ShareCodeRedemptionResult, + args: { + code: t.arg.string({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const shareCode = await db.query.paperShareCode + .findFirst({ + where: { code: args.code } + }) + .then(assertFindFirstExists); + + if (shareCode.permission === 'EDIT') { + // Find conference user for this user in the paper's conference + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: shareCode.paperId }, + with: { committee: true } + }) + .then(assertFindFirstExists); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + conferenceId: paper.committee.conferenceId + } + }) + .then(assertFindFirstExists); + + // Add as editor, ignore if already exists + await db + .insert(schema.paperEditor) + .values({ + paperId: shareCode.paperId, + conferenceUserId: conferenceUser.id + }) + .onConflictDoNothing(); + + paperPubsub.updated(shareCode.paperId); + } + + return { + paperId: shareCode.paperId, + permission: shareCode.permission + }; + } + }) +})); diff --git a/src/api/handlers/paperSponsor.ts b/src/api/handlers/paperSponsor.ts new file mode 100644 index 00000000..51d1d1df --- /dev/null +++ b/src/api/handlers/paperSponsor.ts @@ -0,0 +1,173 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { and, eq } from 'drizzle-orm'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; + +const { arg, ref, pubsub, table } = basics('paperSponsor'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +abilityBuilder.paperSponsor.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.paperSponsor.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + addSponsor: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + committeeMemberId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + // Try chair/admin path first (bypasses all gates) + let isChair = false; + try { + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + isChair = true; + } catch { + // not a chair/admin, will check delegate path below + } + + if (!isChair) { + // Must be a DELEGATE + await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + conferenceUserType: 'DELEGATE' + } + }) + .then(assertFindFirstExists); + + if (paper.status === 'FINAL') { + throw new GraphQLError('Cannot sponsor a finalized paper'); + } + + if (paper.status === 'DRAFT_RESOLUTION' || paper.status === 'AMENDMENT_PHASE') { + const committee = await db.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + + if (!committee.supportReEvaluationOpen) { + throw new GraphQLError('Support re-evaluation is not currently open'); + } + } + } + + const result = await db + .insert(schema.paperSponsor) + .values({ + paperId: args.paperId, + committeeMemberId: args.committeeMemberId + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.paperSponsor + .findFirst( + query( + ctx.abilities.paperSponsor.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + removeSponsor: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }), + committeeMemberId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const sponsor = await db.query.paperSponsor + .findFirst({ + where: { + paperId: args.paperId, + committeeMemberId: args.committeeMemberId + } + }) + .then(assertFindFirstExists); + + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: args.paperId } }) + .then(assertFindFirstExists); + + // Try chair/admin path first (bypasses all gates) + let isChair = false; + try { + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + isChair = true; + } catch { + // not a chair/admin, will check delegate path below + } + + if (!isChair) { + if (paper.status === 'DRAFT_RESOLUTION' || paper.status === 'AMENDMENT_PHASE') { + const committee = await db.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + + if (!committee.supportReEvaluationOpen) { + throw new GraphQLError('Support re-evaluation is not currently open'); + } + } + + // Must be self (removing own sponsorship) or paper creator + const conferenceUser = await db.query.conferenceUser.findFirst({ + where: { + user: { id: user.sub } + } + }); + + const isSelf = conferenceUser?.committeeMemberId === args.committeeMemberId; + + if (!isSelf) { + const isCreator = conferenceUser?.committeeMemberId === paper.creatorCommitteeMemberId; + if (!isCreator) { + throw new GraphQLError( + 'Only the sponsor themselves or the paper creator can remove a sponsor' + ); + } + } + } + + await db + .delete(schema.paperSponsor) + .where( + and( + eq(schema.paperSponsor.paperId, args.paperId), + eq(schema.paperSponsor.committeeMemberId, args.committeeMemberId) + ) + ); + + pubsub.removed(sponsor.id); + paperPubsub.updated(args.paperId); + + return true; + } + }) +})); diff --git a/src/api/handlers/presenceChangedTimestamp.ts b/src/api/handlers/presenceChangedTimestamp.ts index c96dbd24..153e65b4 100644 --- a/src/api/handlers/presenceChangedTimestamp.ts +++ b/src/api/handlers/presenceChangedTimestamp.ts @@ -1,11 +1,9 @@ import { abilityBuilder } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; import { basics } from './basics'; const { arg, ref, pubsub, table } = basics('presenceChangedTimestamp'); -abilityBuilder.presenceChangedTimestamp.allow(['read']).when(({ hasRole }) => { - if (hasRole('admin')) { - return 'allow'; - } +abilityBuilder.presenceChangedTimestamp.allow(['read']).when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); diff --git a/src/api/handlers/register.ts b/src/api/handlers/register.ts index a4401b17..e001908c 100644 --- a/src/api/handlers/register.ts +++ b/src/api/handlers/register.ts @@ -11,3 +11,14 @@ import './time'; import './user'; import './import'; import './presenceChangedTimestamp'; +import './resolutionPaper'; +import './paperSponsor'; +import './paperShareCode'; +import './paperEditor'; +import './paperContentSnapshot'; +import './resolutionComment'; +import './amendment'; +import './amendmentSponsor'; +import './operativeClauseVote'; +import './resolutionVoteResult'; +import './paperClauseLock'; diff --git a/src/api/handlers/representation.ts b/src/api/handlers/representation.ts index ee2ea2f4..920c21ca 100644 --- a/src/api/handlers/representation.ts +++ b/src/api/handlers/representation.ts @@ -1,12 +1,117 @@ -import { abilityBuilder } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder } from '$api/rumble'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { basics } from './basics'; +import { assertConferenceAdmin } from './conferenceUser'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { eq } from 'drizzle-orm'; +import { GraphQLError } from 'graphql'; const { arg, ref, pubsub, table } = basics('representation'); -abilityBuilder.representation.allow(['read', 'update']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +const representationTypeEnum = enum_({ + tsName: 'representationType' }); + +abilityBuilder.representation.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.representation.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); + +schemaBuilder.mutationFields((t) => ({ + createRepresentation: t.drizzleField({ + type: ref, + args: { + conferenceId: t.arg.id({ required: true }), + type: t.arg({ type: representationTypeEnum, required: true }), + name: t.arg.string(), + alpha2Code: t.arg.string(), + alpha3Code: t.arg.string(), + faIcon: t.arg.string() + }, + resolve: async (query, root, args, ctx, info) => { + await assertConferenceAdmin(ctx, args.conferenceId); + + const result = await db + .insert(schema.representation) + .values({ + conferenceId: args.conferenceId, + type: args.type, + name: args.name ?? undefined, + alpha2Code: args.alpha2Code ?? undefined, + alpha3Code: args.alpha3Code ?? undefined, + faIcon: args.faIcon ?? undefined + }) + .returning() + .then(assertFirstEntryExists); + + // For DELEGATION type, auto-create committee members for all committees + if (args.type === 'DELEGATION') { + const committees = await db.query.committee.findMany({ + where: { conferenceId: args.conferenceId } + }); + + if (committees.length > 0) { + await db.insert(schema.committeeMember).values( + committees.map((c) => ({ + committeeId: c.id, + representationId: result.id + })) + ); + } + } + + pubsub.updated(result.id); + + return db.query.representation + .findFirst( + query( + ctx.abilities.representation.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteRepresentation: t.field({ + type: 'Boolean', + args: { + id: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx, info) => { + const representation = await db.query.representation.findFirst({ + where: { id: args.id } + }); + + if (!representation) { + throw new GraphQLError('Representation not found'); + } + + await assertConferenceAdmin(ctx, representation.conferenceId); + + // Delete associated committee members first (FK may not cascade) + await db + .delete(schema.committeeMember) + .where(eq(schema.committeeMember.representationId, args.id)); + + // Delete associated conference members + await db + .delete(schema.conferenceMember) + .where(eq(schema.conferenceMember.representationId, args.id)); + + await db.delete(schema.representation).where(eq(schema.representation.id, args.id)); + + pubsub.removed(args.id); + + return true; + } + }) +})); diff --git a/src/api/handlers/resolutionComment.ts b/src/api/handlers/resolutionComment.ts new file mode 100644 index 00000000..a7ca548b --- /dev/null +++ b/src/api/handlers/resolutionComment.ts @@ -0,0 +1,259 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { eq } from 'drizzle-orm'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; + +const { arg, ref, pubsub, table } = basics('resolutionComment'); +const paperPubsub = rumblePubsub({ table: 'resolutionPaper' }); + +const commentVisibilityEnum = enum_({ tsName: 'commentVisibility' }); + +// Helper: check if user is TEAM/ADMIN for the conference owning a given paper +async function isChairOrAdmin( + ctx: { + hasRole: (role: string) => boolean; + mustBeLoggedIn: () => { sub?: string; email?: string | null }; + }, + committeeId: string +): Promise { + if (isGlobalAdmin(ctx)) return true; + + const user = ctx.mustBeLoggedIn(); + const cuRecord = await db.query.conferenceUser.findFirst({ + where: { + conference: { + committees: { id: committeeId } + }, + user: { id: user.sub }, + conferenceUserType: { in: ['ADMIN', 'TEAM'] } + } + }); + + return !!cuRecord; +} + +// ────────────────────────────────────────────────── +// Access control +// ────────────────────────────────────────────────── + +// Global admin → can see ALL comments (including TEAM_ONLY) +abilityBuilder.resolutionComment.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +// Conference ADMIN/TEAM → can see ALL comments (including TEAM_ONLY) +abilityBuilder.resolutionComment.allow('read').when(((ctx: any) => { + const user = ctx.mustBeLoggedIn(); + if (!user.sub) return; + return db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + conferenceUserType: { in: ['ADMIN', 'TEAM'] } + } + }) + .then((cu: any) => (cu ? 'allow' : undefined)); +}) as any); + +// Regular logged-in users → only see PUBLIC comments +abilityBuilder.resolutionComment.allow('read').when((ctx) => { + ctx.mustBeLoggedIn(); + return { where: { visibility: 'PUBLIC' } }; +}); + +// ────────────────────────────────────────────────── +// Mutations +// ────────────────────────────────────────────────── + +schemaBuilder.mutationFields((t) => ({ + createComment: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + content: t.arg.string({ required: true }), + clauseId: t.arg.string(), + visibility: t.arg({ type: commentVisibilityEnum }), + parentCommentId: t.arg.id() + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Resolve conference user + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { user: { id: user.sub } } + }) + .then(assertFindFirstExists); + + // Fetch paper and validate status + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + const allowedStatuses = ['SUBMITTED', 'DRAFT_RESOLUTION', 'AMENDMENT_PHASE', 'FINAL']; + if (!allowedStatuses.includes(paper.status)) { + throw new GraphQLError( + 'Comments are only allowed on submitted papers and draft resolutions' + ); + } + + // Visibility check: only chairs/admins can post TEAM_ONLY + const visibility = args.visibility ?? 'PUBLIC'; + if (visibility === 'TEAM_ONLY') { + const isChair = await isChairOrAdmin(ctx, paper.committeeId); + if (!isChair) { + throw new GraphQLError('Only chairs and team members can post team-only comments'); + } + } + + // Validate parentCommentId if provided + if (args.parentCommentId) { + const parent = await db.query.resolutionComment + .findFirst({ + where: { id: args.parentCommentId } + }) + .then(assertFindFirstExists); + + if (parent.paperId !== args.paperId) { + throw new GraphQLError('Parent comment must belong to the same paper'); + } + + // Only 1 level of threading: parent must be top-level + if (parent.parentCommentId) { + throw new GraphQLError('Replies can only be one level deep'); + } + + // If clauseId is set, parent must target the same clause + if (args.clauseId && parent.clauseId && parent.clauseId !== args.clauseId) { + throw new GraphQLError('Reply must target the same clause as the parent comment'); + } + } + + const result = await db + .insert(schema.resolutionComment) + .values({ + paperId: args.paperId, + clauseId: args.clauseId ?? null, + authorConferenceUserId: conferenceUser.id, + content: args.content, + visibility, + parentCommentId: args.parentCommentId ?? null + }) + .returning() + .then(assertFirstEntryExists); + + pubsub.created(); + paperPubsub.updated(args.paperId); + + return db.query.resolutionComment + .findFirst( + query( + ctx.abilities.resolutionComment.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + updateComment: t.drizzleField({ + type: ref, + args: { + commentId: t.arg.id({ required: true }), + content: t.arg.string({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const comment = await db.query.resolutionComment + .findFirst({ + where: { id: args.commentId } + }) + .then(assertFindFirstExists); + + // Must be the author + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { user: { id: user.sub } } + }) + .then(assertFindFirstExists); + + if (comment.authorConferenceUserId !== conferenceUser.id) { + throw new GraphQLError('Only the author can edit a comment'); + } + + await db + .update(schema.resolutionComment) + .set({ content: args.content }) + .where(eq(schema.resolutionComment.id, args.commentId)); + + pubsub.updated(args.commentId); + paperPubsub.updated(comment.paperId); + + return db.query.resolutionComment + .findFirst( + query( + ctx.abilities.resolutionComment.filter('read', { + inject: { + where: { id: args.commentId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + deleteComment: t.field({ + type: 'Boolean', + args: { + commentId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx) => { + const user = ctx.mustBeLoggedIn(); + + const comment = await db.query.resolutionComment + .findFirst({ + where: { id: args.commentId } + }) + .then(assertFindFirstExists); + + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { user: { id: user.sub } } + }) + .then(assertFindFirstExists); + + // Author can delete own, or chairs/admins can delete any + const isAuthor = comment.authorConferenceUserId === conferenceUser.id; + if (!isAuthor) { + const paper = await db.query.resolutionPaper + .findFirst({ where: { id: comment.paperId } }) + .then(assertFindFirstExists); + + const isChair = await isChairOrAdmin(ctx, paper.committeeId); + if (!isChair) { + throw new GraphQLError('Only the author or chairs can delete comments'); + } + } + + // Cascade deletes replies via DB constraint + await db + .delete(schema.resolutionComment) + .where(eq(schema.resolutionComment.id, args.commentId)); + + pubsub.removed(args.commentId); + paperPubsub.updated(comment.paperId); + + return true; + } + }) +})); diff --git a/src/api/handlers/resolutionPaper.ts b/src/api/handlers/resolutionPaper.ts new file mode 100644 index 00000000..9e41eaad --- /dev/null +++ b/src/api/handlers/resolutionPaper.ts @@ -0,0 +1,929 @@ +import { db, schema } from '$api/db/db'; +import { abilityBuilder, enum_, schemaBuilder, pubsub as rumblePubsub } from '$api/rumble'; +import { and, eq, isNull, count as drizzleCount, desc, inArray } from 'drizzle-orm'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; +import { GraphQLError } from 'graphql'; +import { + ResolutionSchema, + createEmptyResolution, + toRoman +} from '@deutschemodelunitednations/munify-resolution-editor/schema'; + +const { arg, ref, pubsub, table } = basics('resolutionPaper'); +const committeePubsub = rumblePubsub({ table: 'committee' }); +const voteResultPubsub = rumblePubsub({ table: 'resolutionVoteResult' }); +const clauseVotePubsub = rumblePubsub({ table: 'operativeClauseVote' }); + +const paperStatusEnum = enum_({ tsName: 'paperStatus' }); + +abilityBuilder.resolutionPaper.allow(['read', 'update']).when((ctx) => { + if (isGlobalAdmin(ctx)) { + return { where: { deletedAt: { isNull: true } } }; + } +}); + +abilityBuilder.resolutionPaper.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return { where: { deletedAt: { isNull: true } } }; +}); + +/** + * Helper to check if the current user is a chair (ADMIN/TEAM) for a committee's conference, + * or a global admin. + */ +export async function assertCommitteeChairOrAdmin( + ctx: { + hasRole: (role: string) => boolean; + mustBeLoggedIn: () => { sub?: string; email?: string | null }; + }, + committeeId: string +) { + if (isGlobalAdmin(ctx)) { + return; + } + + const user = ctx.mustBeLoggedIn(); + + await db.query.conferenceUser + .findFirst({ + where: { + conference: { + committees: { + id: committeeId + } + }, + user: { + id: user.sub + }, + conferenceUserType: { + in: ['ADMIN', 'TEAM'] + } + } + }) + .then(assertFindFirstExists); +} + +schemaBuilder.mutationFields((t) => ({ + createResolutionPaper: t.drizzleField({ + type: ref, + args: { + committeeId: t.arg.id({ required: true }), + agendaItemId: t.arg.id({ required: true }), + title: t.arg.string() + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Must be a DELEGATE with a committeeMember in this committee + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + conferenceUserType: 'DELEGATE', + committeeMember: { + committeeId: args.committeeId + } + } + }) + .then(assertFindFirstExists); + + if (!conferenceUser.committeeMemberId) { + throw new GraphQLError('You must be assigned to a committee member'); + } + + // Get committee name for the empty resolution + const committee = await db.query.committee + .findFirst({ + where: { id: args.committeeId } + }) + .then(assertFindFirstExists); + + const result = await db + .insert(schema.resolutionPaper) + .values({ + committeeId: args.committeeId, + agendaItemId: args.agendaItemId, + creatorCommitteeMemberId: conferenceUser.committeeMemberId, + title: args.title ?? undefined, + content: createEmptyResolution(committee.name) + }) + .returning() + .then(assertFirstEntryExists); + + // Auto-add creator as sponsor + await db.insert(schema.paperSponsor).values({ + paperId: result.id, + committeeMemberId: conferenceUser.committeeMemberId + }); + + // Auto-add creator as editor + await db.insert(schema.paperEditor).values({ + paperId: result.id, + conferenceUserId: conferenceUser.id + }); + + pubsub.created(); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + chairCreateResolutionPaper: t.drizzleField({ + type: ref, + args: { + committeeId: t.arg.id({ required: true }), + agendaItemId: t.arg.id({ required: true }), + committeeMemberId: t.arg.id({ required: true }), + title: t.arg.string() + }, + resolve: async (query, root, args, ctx, info) => { + await assertCommitteeChairOrAdmin(ctx, args.committeeId); + + // Validate committeeMemberId belongs to this committee + const committeeMember = await db.query.committeeMember + .findFirst({ + where: { id: args.committeeMemberId, committeeId: args.committeeId } + }) + .then(assertFindFirstExists); + + // Get committee name for the empty resolution + const committee = await db.query.committee + .findFirst({ + where: { id: args.committeeId } + }) + .then(assertFindFirstExists); + + const content = createEmptyResolution(committee.name); + + const result = await db.transaction(async (tx) => { + const paper = await tx + .insert(schema.resolutionPaper) + .values({ + committeeId: args.committeeId, + agendaItemId: args.agendaItemId, + creatorCommitteeMemberId: args.committeeMemberId, + title: args.title ?? undefined, + content, + status: 'SUBMITTED' + }) + .returning() + .then(assertFirstEntryExists); + + // Auto-add creator as sponsor + await tx.insert(schema.paperSponsor).values({ + paperId: paper.id, + committeeMemberId: args.committeeMemberId + }); + + // Auto-add creator as editor (find their conferenceUser) + const conferenceUser = await tx.query.conferenceUser.findFirst({ + where: { committeeMemberId: args.committeeMemberId } + }); + + if (conferenceUser) { + await tx.insert(schema.paperEditor).values({ + paperId: paper.id, + conferenceUserId: conferenceUser.id + }); + } + + // Create content snapshot for submission + await tx.insert(schema.paperContentSnapshot).values({ + paperId: paper.id, + content, + trigger: 'SUBMITTED' + }); + + return paper; + }); + + pubsub.created(); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: result.id } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + updatePaperContent: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + content: t.arg({ type: 'JSON', required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + // Validate content against schema + const parsed = ResolutionSchema.safeParse(args.content); + if (!parsed.success) { + throw new GraphQLError('Invalid resolution content: ' + parsed.error.message); + } + + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + // Status-dependent auth + if (paper.status === 'DRAFT_RESOLUTION' || paper.status === 'AMENDMENT_PHASE') { + // Only chair/admin can edit DRs + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + } else if (paper.status === 'SUBMITTED') { + // Chair/admin OR creator + editors + const isChair = await db.query.conferenceUser.findFirst({ + where: { + conference: { + committees: { id: paper.committeeId } + }, + user: { id: user.sub }, + conferenceUserType: { in: ['ADMIN', 'TEAM'] } + } + }); + + if (!isChair && !ctx.hasRole('admin')) { + // Must be creator or editor + const isEditor = await db.query.paperEditor.findFirst({ + where: { + paperId: args.paperId, + conferenceUser: { user: { id: user.sub } } + } + }); + + if (!isEditor) { + throw new GraphQLError('You do not have permission to edit this paper'); + } + } + } else if (paper.status === 'WORKING_PAPER') { + // Creator + editors + const isEditor = await db.query.paperEditor.findFirst({ + where: { + paperId: args.paperId, + conferenceUser: { user: { id: user.sub } } + } + }); + + if (!isEditor && !ctx.hasRole('admin')) { + throw new GraphQLError('You do not have permission to edit this paper'); + } + } else { + throw new GraphQLError('Paper cannot be edited in its current status'); + } + + // Resolve sender's conferenceUserId for lock-aware merge + const senderConferenceUser = await db.query.conferenceUser.findFirst({ + where: { user: { id: user.sub } } + }); + + let contentToWrite = parsed.data; + + if (senderConferenceUser) { + // Fetch active (non-expired) locks held by OTHER users + const expiryThreshold = new Date(Date.now() - 60_000); + const otherLocks = await db.query.paperClauseLock.findMany({ + where: { + paperId: args.paperId, + conferenceUserId: { ne: senderConferenceUser.id }, + acquiredAt: { gte: expiryThreshold } + } + }); + + if (otherLocks.length > 0 && paper.content) { + const othersLockedClauseIds = new Set(otherLocks.map((l) => l.clauseId)); + const currentContent = ResolutionSchema.safeParse(paper.content); + + if (currentContent.success) { + const dbContent = currentContent.data; + const incoming = parsed.data; + + // Build map of DB clauses by ID + const dbPreambleMap = new Map(dbContent.preamble.map((c) => [c.id, c])); + const dbOperativeMap = new Map(dbContent.operative.map((c) => [c.id, c])); + + // Merge preamble: for locked clauses, keep DB version + const mergedPreamble = incoming.preamble.map((clause) => { + if (othersLockedClauseIds.has(clause.id) && dbPreambleMap.has(clause.id)) { + return dbPreambleMap.get(clause.id)!; + } + return clause; + }); + + // Append preamble clauses locked by others that sender deleted + for (const [id, clause] of dbPreambleMap) { + if (othersLockedClauseIds.has(id) && !incoming.preamble.some((c) => c.id === id)) { + mergedPreamble.push(clause); + } + } + + // Merge operative: for locked clauses, keep DB version + const mergedOperative = incoming.operative.map((clause) => { + if (othersLockedClauseIds.has(clause.id) && dbOperativeMap.has(clause.id)) { + return dbOperativeMap.get(clause.id)!; + } + return clause; + }); + + // Append operative clauses locked by others that sender deleted + for (const [id, clause] of dbOperativeMap) { + if (othersLockedClauseIds.has(id) && !incoming.operative.some((c) => c.id === id)) { + mergedOperative.push(clause); + } + } + + contentToWrite = { + committeeName: incoming.committeeName, + preamble: mergedPreamble, + operative: mergedOperative + }; + } + } + + // Refresh sender's locks (keep alive during active editing) + await db + .update(schema.paperClauseLock) + .set({ acquiredAt: new Date() }) + .where( + and( + eq(schema.paperClauseLock.paperId, args.paperId), + eq(schema.paperClauseLock.conferenceUserId, senderConferenceUser.id) + ) + ); + } + + await db + .update(schema.resolutionPaper) + .set({ content: contentToWrite }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + pubsub.updated(args.paperId); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + updatePaperTitle: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + title: t.arg.string({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'WORKING_PAPER') { + throw new GraphQLError('Title can only be changed for working papers'); + } + + // Must be creator or editor + const isEditor = await db.query.paperEditor.findFirst({ + where: { + paperId: args.paperId, + conferenceUser: { user: { id: user.sub } } + } + }); + + if (!isEditor && !ctx.hasRole('admin')) { + throw new GraphQLError('You do not have permission to edit this paper'); + } + + await db + .update(schema.resolutionPaper) + .set({ title: args.title }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + pubsub.updated(args.paperId); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + submitPaper: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'WORKING_PAPER') { + throw new GraphQLError('Only working papers can be submitted'); + } + + // Only creator can submit + const conferenceUser = await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: paper.creatorCommitteeMemberId + } + }) + .then(assertFindFirstExists); + + await db.transaction(async (tx) => { + await tx + .update(schema.resolutionPaper) + .set({ status: 'SUBMITTED' }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + // Create content snapshot + await tx.insert(schema.paperContentSnapshot).values({ + paperId: args.paperId, + content: paper.content, + trigger: 'SUBMITTED' + }); + + // Remove all clause locks so the chair can edit freely + await tx + .delete(schema.paperClauseLock) + .where(eq(schema.paperClauseLock.paperId, args.paperId)); + }); + + pubsub.updated(args.paperId); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + promoteToDraftResolution: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'SUBMITTED') { + throw new GraphQLError('Only submitted papers can be promoted to draft resolutions'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + const committee = await db.query.committee + .findFirst({ + where: { id: paper.committeeId } + }) + .then(assertFindFirstExists); + + // Get agenda item position for numbering + const agendaItems = await db.query.agendaItem.findMany({ + where: { committeeId: paper.committeeId } + }); + const agendaItemIndex = agendaItems.findIndex((ai) => ai.id === paper.agendaItemId); + const agendaPosition = agendaItemIndex >= 0 ? agendaItemIndex + 1 : 1; + + // Count existing DRs for this agenda item for sequence number + const existingDRsForItem = await db + .select({ count: drizzleCount() }) + .from(schema.resolutionPaper) + .where( + and( + eq(schema.resolutionPaper.agendaItemId, paper.agendaItemId), + eq(schema.resolutionPaper.status, 'DRAFT_RESOLUTION'), + isNull(schema.resolutionPaper.deletedAt) + ) + ) + .then(assertFirstEntryExists); + + const sequenceNumber = existingDRsForItem.count + 1; + const documentNumber = `${committee.abbreviation}/${toRoman(agendaPosition)}/DR.${sequenceNumber}`; + + await db.transaction(async (tx) => { + await tx + .update(schema.resolutionPaper) + .set({ + status: 'DRAFT_RESOLUTION', + documentNumber, + sequenceNumber + }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + // Create content snapshot + await tx.insert(schema.paperContentSnapshot).values({ + paperId: args.paperId, + content: paper.content, + trigger: 'DRAFT_RESOLUTION' + }); + }); + + pubsub.updated(args.paperId); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + startVotingPhase: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'AMENDMENT_PHASE') { + throw new GraphQLError('Paper must be in AMENDMENT_PHASE to start voting'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db.transaction(async (tx) => { + await tx + .update(schema.resolutionPaper) + .set({ status: 'VOTING_PHASE' }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + await tx.insert(schema.paperContentSnapshot).values({ + paperId: args.paperId, + content: paper.content, + trigger: 'VOTING_PHASE' + }); + + // Reset currentOperativeIndex to 0 for voting navigation + const parsed = ResolutionSchema.safeParse(paper.content); + const firstClauseId = parsed.success ? (parsed.data.operative[0]?.id ?? null) : null; + await tx + .update(schema.committee) + .set({ currentOperativeIndex: 0, currentOperativeClauseId: firstClauseId }) + .where(eq(schema.committee.id, paper.committeeId)); + }); + + pubsub.updated(args.paperId); + committeePubsub.updated(paper.committeeId); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + recordVoteResult: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + outcome: t.arg({ type: enum_({ tsName: 'voteOutcome' }), required: true }), + votesFor: t.arg.int({ required: true }), + votesAgainst: t.arg.int({ required: true }), + votesAbstain: t.arg.int() + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'VOTING_PHASE' && paper.status !== 'AMENDMENT_PHASE') { + throw new GraphQLError('Paper must be in VOTING_PHASE to record final vote'); + } + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + await db.transaction(async (tx) => { + await tx.insert(schema.resolutionVoteResult).values({ + paperId: args.paperId, + outcome: args.outcome, + votesFor: args.votesFor, + votesAgainst: args.votesAgainst, + votesAbstain: args.votesAbstain ?? 0 + }); + + const updateSet: { status: 'FINAL'; content?: unknown; documentNumber?: string } = { + status: 'FINAL' + }; + + if (args.outcome === 'ADOPTED') { + const rejectedVotes = await tx.query.operativeClauseVote.findMany({ + where: { paperId: args.paperId, outcome: 'REJECTED' } + }); + const rejectedIds = new Set(rejectedVotes.map((v) => v.clauseId)); + + if (rejectedIds.size > 0) { + const parsed = ResolutionSchema.safeParse(paper.content); + if (parsed.success) { + parsed.data.operative = parsed.data.operative.filter( + (clause) => !rejectedIds.has(clause.id) + ); + updateSet.content = parsed.data; + } + } + + // Change DR to RES in document number + if (paper.documentNumber) { + updateSet.documentNumber = paper.documentNumber.replace('/DR.', '/RES.'); + } + } + + await tx + .update(schema.resolutionPaper) + .set(updateSet) + .where(eq(schema.resolutionPaper.id, args.paperId)); + }); + + // Always clear activeDraftResolutionId and currentOperativeIndex + const updateSet: Record = { + activeDraftResolutionId: null, + currentOperativeIndex: null, + currentOperativeClauseId: null + }; + + if (args.outcome === 'ADOPTED') { + updateSet.lastResolutionAdoptionDate = new Date(); + } + + await db + .update(schema.committee) + .set(updateSet) + .where(eq(schema.committee.id, paper.committeeId)); + + committeePubsub.updated(paper.committeeId); + pubsub.updated(args.paperId); + voteResultPubsub.created(); + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + + softDeletePaper: t.field({ + type: 'Boolean', + args: { + paperId: t.arg.id({ required: true }) + }, + resolve: async (root, args, ctx) => { + const user = ctx.mustBeLoggedIn(); + + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + if (paper.status !== 'WORKING_PAPER') { + throw new GraphQLError('Only working papers can be deleted'); + } + + // Only creator can delete + await db.query.conferenceUser + .findFirst({ + where: { + user: { id: user.sub }, + committeeMemberId: paper.creatorCommitteeMemberId + } + }) + .then(assertFindFirstExists); + + await db + .update(schema.resolutionPaper) + .set({ deletedAt: new Date() }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + pubsub.updated(args.paperId); + + return true; + } + }), + + revertPaperStatus: t.drizzleField({ + type: ref, + args: { + paperId: t.arg.id({ required: true }), + restoreSnapshot: t.arg.boolean() + }, + resolve: async (query, root, args, ctx, info) => { + const paper = await db.query.resolutionPaper + .findFirst({ + where: { id: args.paperId } + }) + .then(assertFindFirstExists); + + await assertCommitteeChairOrAdmin(ctx, paper.committeeId); + + const statusOrder = [ + 'WORKING_PAPER', + 'SUBMITTED', + 'DRAFT_RESOLUTION', + 'AMENDMENT_PHASE', + 'VOTING_PHASE', + 'FINAL' + ] as const; + const currentIndex = statusOrder.indexOf(paper.status as (typeof statusOrder)[number]); + if (currentIndex <= 0) { + throw new GraphQLError('Paper is already at initial status and cannot be reverted'); + } + const targetStatus = statusOrder[currentIndex - 1]; + + await db.transaction(async (tx) => { + // Status-specific side effects + if (paper.status === 'FINAL') { + // Delete the resolution vote result + await tx + .delete(schema.resolutionVoteResult) + .where(eq(schema.resolutionVoteResult.paperId, args.paperId)); + // Restore as active DR if committee has none + const committee = await tx.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + if (!committee.activeDraftResolutionId) { + const parsed = ResolutionSchema.safeParse(paper.content); + const firstClauseId = parsed.success ? (parsed.data.operative[0]?.id ?? null) : null; + await tx + .update(schema.committee) + .set({ + activeDraftResolutionId: args.paperId, + currentOperativeIndex: 0, + currentOperativeClauseId: firstClauseId + }) + .where(eq(schema.committee.id, paper.committeeId)); + } + } else if (paper.status === 'VOTING_PHASE') { + // Delete all operative clause votes for this paper + await tx + .delete(schema.operativeClauseVote) + .where(eq(schema.operativeClauseVote.paperId, args.paperId)); + } else if (paper.status === 'AMENDMENT_PHASE') { + // Clear currentOperativeIndex on committee + await tx + .update(schema.committee) + .set({ currentOperativeIndex: null, currentOperativeClauseId: null }) + .where(eq(schema.committee.id, paper.committeeId)); + if (args.restoreSnapshot) { + // Restore content from latest AMENDMENT_PHASE snapshot + const snapshot = await tx.query.paperContentSnapshot.findFirst({ + where: { paperId: args.paperId, trigger: 'AMENDMENT_PHASE' }, + orderBy: { createdAt: 'desc' } + }); + if (snapshot?.content) { + await tx + .update(schema.resolutionPaper) + .set({ content: snapshot.content }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + } + // Reset applied amendments back to PENDING + await tx + .update(schema.amendment) + .set({ status: 'PENDING' }) + .where( + and( + eq(schema.amendment.paperId, args.paperId), + inArray(schema.amendment.status, ['CONSENSUS_ADOPTED', 'ACCEPTED']) + ) + ); + } + } else if (paper.status === 'DRAFT_RESOLUTION') { + // Clear document number and sequence + await tx + .update(schema.resolutionPaper) + .set({ documentNumber: null, sequenceNumber: null }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + // Clear active DR if this paper was active + const committee = await tx.query.committee + .findFirst({ where: { id: paper.committeeId } }) + .then(assertFindFirstExists); + if (committee.activeDraftResolutionId === args.paperId) { + await tx + .update(schema.committee) + .set({ + activeDraftResolutionId: null, + currentOperativeIndex: null, + currentOperativeClauseId: null + }) + .where(eq(schema.committee.id, paper.committeeId)); + } + } + // SUBMITTED → WORKING_PAPER: no side effects + + // Update the paper status + await tx + .update(schema.resolutionPaper) + .set({ status: targetStatus }) + .where(eq(schema.resolutionPaper.id, args.paperId)); + + // Create audit snapshot + await tx.insert(schema.paperContentSnapshot).values({ + paperId: args.paperId, + content: paper.content, + trigger: `REVERT_FROM_${paper.status}` + }); + }); + + pubsub.updated(args.paperId); + committeePubsub.updated(paper.committeeId); + + // Notify vote subscriptions when reverting from statuses that delete votes + if (paper.status === 'FINAL') { + voteResultPubsub.created(); + } else if (paper.status === 'VOTING_PHASE') { + clauseVotePubsub.created(); + } + + return db.query.resolutionPaper + .findFirst( + query( + ctx.abilities.resolutionPaper.filter('read', { + inject: { + where: { id: args.paperId } + } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }) +})); diff --git a/src/api/handlers/resolutionVoteResult.ts b/src/api/handlers/resolutionVoteResult.ts new file mode 100644 index 00000000..dd8d80ea --- /dev/null +++ b/src/api/handlers/resolutionVoteResult.ts @@ -0,0 +1,14 @@ +import { abilityBuilder } from '$api/rumble'; +import { basics } from './basics'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; + +const { arg, ref, pubsub, table } = basics('resolutionVoteResult'); + +abilityBuilder.resolutionVoteResult.allow('read').when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.resolutionVoteResult.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; +}); diff --git a/src/api/handlers/speakerOnList.ts b/src/api/handlers/speakerOnList.ts index da8e2f8b..b8b070b1 100644 --- a/src/api/handlers/speakerOnList.ts +++ b/src/api/handlers/speakerOnList.ts @@ -5,7 +5,8 @@ import { db, schema } from '$api/db/db'; import { and, count, eq, gt, gte, sql } from 'drizzle-orm'; import { assertFindFirstExists, assertFirstEntryExists } from '@m1212e/rumble'; import { SpeakersListRef } from './speakersList'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; const { arg, ref, pubsub, table } = basics('speakerOnList'); @@ -15,11 +16,13 @@ export const SpeakerOnWhereArgs = arg; // TODO: These could use some validation for the position values. E.g. only allow positons // which are in bounds and so on -abilityBuilder.speakerOnList.allow(['read', 'update', 'delete']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.speakerOnList.allow(['read', 'update', 'delete']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.speakerOnList.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); schemaBuilder.mutationFields((t) => { @@ -31,17 +34,24 @@ schemaBuilder.mutationFields((t) => { overwriteName: t.arg.string() }, resolve: async (query, _root, args, ctx, _info) => { + // Verify chair/admin access + const speaker = await db.query.speakerOnList + .findFirst({ + where: { id: args.id }, + with: { speakersList: { with: { agendaItem: { columns: { committeeId: true } } } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin( + ctx, + (speaker as any).speakersList.agendaItem.committeeId + ); + const updated = await db .update(table) .set({ overwriteName: args.overwriteName ? args.overwriteName : null }) - .where( - and( - eq(schema.speakerOnList.id, args.id), - ctx.abilities.speakerOnList.filter('update').sql.where - ) - ) + .where(eq(schema.speakerOnList.id, args.id)) .returning() .then(assertFirstEntryExists); @@ -77,6 +87,15 @@ schemaBuilder.mutationFields((t) => { throw new GraphQLError('Must set either committeeMemberId or conferenceMemberId'); } + // Verify chair/admin access + const speakersListForAuth = await db.query.speakersList + .findFirst({ + where: { id: args.speakersListId }, + with: { agendaItem: { columns: { committeeId: true } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin(ctx, (speakersListForAuth as any).agendaItem.committeeId); + const createdId = await db.transaction(async (tx) => { let position = args.position; if (!position) { @@ -97,21 +116,12 @@ schemaBuilder.mutationFields((t) => { position: sql`${table.position} + 1` }) .where( - and( - eq(table.speakersListId, args.speakersListId), - gte(table.position, position), - ctx.abilities.speakerOnList.filter('update').sql.where - ) + and(eq(table.speakersListId, args.speakersListId), gte(table.position, position)) ); } - // we do query this for checking the required permissions const speakersList = await tx.query.speakersList - .findFirst( - ctx.abilities.speakersList.filter('update', { - inject: { where: { id: args.speakersListId } } - }).query.single - ) + .findFirst({ where: { id: args.speakersListId } }) .then(assertFindFirstExists); const created = await tx @@ -148,18 +158,251 @@ schemaBuilder.mutationFields((t) => { speakerOnListId: t.arg.id({ required: true }) }, resolve: async (query, root, args, ctx, info) => { + // Verify chair/admin access + const speaker = await db.query.speakerOnList + .findFirst({ + where: { id: args.speakerOnListId }, + with: { speakersList: { with: { agendaItem: { columns: { committeeId: true } } } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin( + ctx, + (speaker as any).speakersList.agendaItem.committeeId + ); + const removed = await db.transaction(async (tx) => { const deleted = await tx .delete(table) - .where( - and( - eq(table.id, args.speakerOnListId), - ctx.abilities.speakerOnList.filter('delete').sql.where - ) + .where(eq(table.id, args.speakerOnListId)) + .returning() + .then(assertFirstEntryExists); + + const aboutToBeShiftedDown = await tx.query.speakerOnList.findMany({ + where: { + speakersListId: deleted.speakersListId, + position: { + gt: deleted.position + } + }, + orderBy: { position: 'asc' } + }); + + for (const speaker of aboutToBeShiftedDown) { + await tx + .update(table) + .set({ + position: sql`${table.position} - 1` + }) + .where(eq(table.id, speaker.id)); + } + + return deleted; + }); + pubsub.removed(removed.id); + + return db.query.speakersList + .findFirst( + query( + ctx.abilities.speakersList.filter('read', { + inject: { where: { id: removed.speakersListId } } + }).query.single + ) + ) + .then(assertFindFirstExists); + } + }), + selfAddToSpeakersList: t.drizzleField({ + type: ref, + args: { + speakersListId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + if (!user.email) { + throw new GraphQLError('User email is required'); + } + + const createdId = await db.transaction(async (tx) => { + // Find the user's conferenceUser record + const conferenceUser = await tx.query.conferenceUser.findFirst({ + where: { userEmail: user.email! }, + with: { + committeeMember: { + with: { committee: true } + }, + conferenceMember: true + } + }); + + if (!conferenceUser) { + throw new GraphQLError('Conference user not found'); + } + + if ( + conferenceUser.conferenceUserType !== 'DELEGATE' && + conferenceUser.conferenceUserType !== 'NON_STATE_ACTOR' + ) { + throw new GraphQLError( + 'Only delegates and non-state actors can self-add to speakers lists' + ); + } + + // Get the speakers list and traverse to committee to check the flag + const speakersList = await tx.query.speakersList.findFirst({ + where: { id: args.speakersListId }, + with: { + agendaItem: { + with: { + committee: true + } + } + } + }); + + if (!speakersList) { + throw new GraphQLError('Speakers list not found'); + } + + if (speakersList.isClosed) { + throw new GraphQLError('Speakers list is closed'); + } + + const committee = (speakersList as any).agendaItem?.committee; + if (!committee) { + throw new GraphQLError('Committee not found for this speakers list'); + } + if (!committee.allowDelegationsToAddThemselvesToSpeakersList) { + throw new GraphQLError( + 'Self-adding to speakers list is not enabled for this committee' + ); + } + + let committeeMemberId: string | null = null; + let conferenceMemberId: string | null = null; + + const confUser = conferenceUser as any; + if (conferenceUser.conferenceUserType === 'DELEGATE') { + if (!confUser.committeeMember) { + throw new GraphQLError('Delegate is not assigned to a committee'); + } + if (!confUser.committeeMember.present) { + throw new GraphQLError('Delegate must be marked as present to add to speakers list'); + } + if (confUser.committeeMember.committeeId !== committee.id) { + throw new GraphQLError('Delegate is not a member of this committee'); + } + committeeMemberId = confUser.committeeMember.id; + } else { + // NON_STATE_ACTOR + if (!confUser.conferenceMember) { + throw new GraphQLError('Non-state actor is not assigned a conference member'); + } + conferenceMemberId = confUser.conferenceMember.id; + } + + // Check not already on list + const existing = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + ...(committeeMemberId ? { committeeMemberId } : {}), + ...(conferenceMemberId ? { conferenceMemberId } : {}) + } + }); + + if (existing) { + throw new GraphQLError('Already on speakers list'); + } + + // Append at end + const position = ( + await tx + .select({ count: count() }) + .from(table) + .where(eq(table.speakersListId, args.speakersListId)) + .then(assertFirstEntryExists) + ).count; + + const created = await tx + .insert(table) + .values({ + committeeMemberId, + conferenceMemberId, + speakersListId: args.speakersListId, + position + }) + .returning({ id: table.id }) + .then(assertFirstEntryExists); + + return created.id; + }); + + pubsub.created(); + + return db.query.speakerOnList + .findFirst( + query( + ctx.abilities.speakerOnList.filter('read', { + inject: { where: { id: createdId } } + }).query.single ) + ) + .then(assertFindFirstExists); + } + }), + selfRemoveFromSpeakersList: t.drizzleField({ + type: SpeakersListRef, + args: { + speakersListId: t.arg.id({ required: true }) + }, + resolve: async (query, root, args, ctx, info) => { + const user = ctx.mustBeLoggedIn(); + if (!user.email) { + throw new GraphQLError('User email is required'); + } + + const removed = await db.transaction(async (tx) => { + const conferenceUser = await tx.query.conferenceUser.findFirst({ + where: { userEmail: user.email! }, + with: { + committeeMember: true, + conferenceMember: true + } + }); + + if (!conferenceUser) { + throw new GraphQLError('Conference user not found'); + } + + // Find own speaker entry on this list + let speakerOnList; + if (conferenceUser.committeeMemberId) { + speakerOnList = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + committeeMemberId: conferenceUser.committeeMemberId + } + }); + } + if (!speakerOnList && conferenceUser.conferenceMemberId) { + speakerOnList = await tx.query.speakerOnList.findFirst({ + where: { + speakersListId: args.speakersListId, + conferenceMemberId: conferenceUser.conferenceMemberId + } + }); + } + + if (!speakerOnList) { + throw new GraphQLError('You are not on this speakers list'); + } + + const deleted = await tx + .delete(table) + .where(eq(table.id, speakerOnList.id)) .returning() .then(assertFirstEntryExists); + // Shift positions down const aboutToBeShiftedDown = await tx.query.speakerOnList.findMany({ where: { speakersListId: deleted.speakersListId, @@ -181,6 +424,7 @@ schemaBuilder.mutationFields((t) => { return deleted; }); + pubsub.removed(removed.id); return db.query.speakersList @@ -204,12 +448,22 @@ schemaBuilder.mutationFields((t) => { if (args.position < 0) { throw new GraphQLError('Position must be a non-negative integer'); } + + // Verify chair/admin access + const speakerForAuth = await db.query.speakerOnList + .findFirst({ + where: { id: args.id }, + with: { speakersList: { with: { agendaItem: { columns: { committeeId: true } } } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin( + ctx, + (speakerForAuth as any).speakersList.agendaItem.committeeId + ); + const updatedEntityIds = await db.transaction(async (tx) => { const aboutToMoveSpeakerOnList = await tx.query.speakerOnList - .findFirst( - ctx.abilities.speakerOnList.filter('update', { inject: { where: { id: args.id } } }) - .query.single - ) + .findFirst({ where: { id: args.id } }) .then(assertFindFirstExists); if (args.position === aboutToMoveSpeakerOnList.position) { throw new GraphQLError('Cannot move to the same position'); diff --git a/src/api/handlers/speakersList.ts b/src/api/handlers/speakersList.ts index 55040f2b..f477ad84 100644 --- a/src/api/handlers/speakersList.ts +++ b/src/api/handlers/speakersList.ts @@ -7,12 +7,13 @@ import { object, query } from '$api/rumble'; -import { and, eq } from 'drizzle-orm'; +import { eq } from 'drizzle-orm'; import { basics } from './basics'; import { assertFindFirstExists } from '@m1212e/rumble'; import { GraphQLError } from 'graphql'; import { SpeakerOnListRef, SpeakerOnWhereArgs } from './speakerOnList'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; +import { assertCommitteeChairOrAdmin } from './resolutionPaper'; // const { arg, ref, pubsub: speakersListPubSub, table } = basics('speakersList'); @@ -49,11 +50,13 @@ query({ table: 'speakersList' }); -abilityBuilder.speakersList.allow(['read', 'update', 'delete']).when(({ mustBeLoggedIn }) => { - const user = mustBeLoggedIn(); - if (user?.email && isWhitelistedEmail(user.email)) { - return 'allow'; - } +abilityBuilder.speakersList.allow(['read', 'update', 'delete']).when((ctx) => { + if (isGlobalAdmin(ctx)) return 'allow'; +}); + +abilityBuilder.speakersList.allow('read').when(({ mustBeLoggedIn }) => { + mustBeLoggedIn(); + return 'allow'; }); schemaBuilder.mutationFields((t) => { @@ -78,6 +81,15 @@ schemaBuilder.mutationFields((t) => { throw new GraphQLError('startTimestamp and stopTimer are mutually exclusive'); } + // Verify chair/admin access via speakersList → agendaItem → committee + const sl = await db.query.speakersList + .findFirst({ + where: { id: args.id }, + with: { agendaItem: { columns: { committeeId: true } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin(ctx, (sl as any).agendaItem.committeeId); + await db.transaction(async (tx) => { if (args.stopTimer) { const speakersList = await tx.query.speakersList @@ -115,12 +127,7 @@ schemaBuilder.mutationFields((t) => { startTimestamp: args.stopTimer ? null : (args.startTimestamp ?? undefined), isClosed: args.isClosed ?? undefined }) - .where( - and( - eq(schema.speakersList.id, args.id), - ctx.abilities.speakersList.filter('update').sql.where - ) - ); + .where(eq(schema.speakersList.id, args.id)); }); speakersListPubSub.updated(args.id); @@ -142,14 +149,18 @@ schemaBuilder.mutationFields((t) => { id: t.arg.id({ required: true }) }, resolve: async (query, root, args, ctx, info) => { + // Verify chair/admin access + const sl = await db.query.speakersList + .findFirst({ + where: { id: args.id }, + with: { agendaItem: { columns: { committeeId: true } } } + }) + .then(assertFindFirstExists); + await assertCommitteeChairOrAdmin(ctx, (sl as any).agendaItem.committeeId); + const deleted = await db .delete(schema.speakerOnList) - .where( - and( - eq(schema.speakerOnList.speakersListId, args.id), - ctx.abilities.speakerOnList.filter('delete').sql.where - ) - ) + .where(eq(schema.speakerOnList.speakersListId, args.id)) .returning(); if (deleted.length > 0) { diff --git a/src/api/handlers/user.ts b/src/api/handlers/user.ts index 337767bd..0a35c9b6 100644 --- a/src/api/handlers/user.ts +++ b/src/api/handlers/user.ts @@ -1,6 +1,6 @@ import { schema } from '$api/db/db'; import { abilityBuilder, schemaBuilder } from '$api/rumble'; -import { isWhitelistedEmail } from '$api/services/isDMUNEmail'; +import { isGlobalAdmin } from '$api/services/isAdminEmail'; import { basics } from './basics'; import { and, eq } from 'drizzle-orm'; @@ -22,3 +22,11 @@ abilityBuilder.user.allow('read'); // return 'allow'; // } // }); + +schemaBuilder.queryFields((t) => ({ + isGlobalAdmin: t.boolean({ + resolve: (root, args, ctx) => { + return isGlobalAdmin(ctx); + } + }) +})); diff --git a/src/api/rumble.ts b/src/api/rumble.ts index 39505122..8a406a77 100644 --- a/src/api/rumble.ts +++ b/src/api/rumble.ts @@ -2,6 +2,20 @@ import { rumble } from '@m1212e/rumble'; import { db } from './db/db'; import { context } from './context'; import { dev } from '$app/environment'; +import { Redis } from 'ioredis'; +import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'; +import { configPrivate } from '$config/private'; + +let eventTarget: ReturnType | undefined; +if (configPrivate.REDIS_URL) { + const publishClient = new Redis(configPrivate.REDIS_URL); + const subscribeClient = new Redis(configPrivate.REDIS_URL); + + eventTarget = createRedisEventTarget({ + publishClient, + subscribeClient + }); +} // this tells the dev server to reload the cache of the schema builder to prevent buildup of non // existent fields/queries @@ -13,5 +27,6 @@ export const { abilityBuilder, schemaBuilder, arg, object, query, pubsub, create rumble({ db, context, - defaultLimit: 300 + defaultLimit: 1000, + subscriptions: [{ eventTarget }] }); diff --git a/src/api/services/OIDC.ts b/src/api/services/OIDC.ts index 5289d271..f2fb0354 100644 --- a/src/api/services/OIDC.ts +++ b/src/api/services/OIDC.ts @@ -4,36 +4,64 @@ import { configPrivate } from '$config/private'; import { configPublic } from '$config/public'; import { makeOIDC } from '@m1212e/sveltekit-oidc'; +/** + * Normalize OIDC claims from different providers into a consistent shape. + * Logto uses `username` instead of `preferred_username` and `name` instead of `family_name`/`given_name`. + */ +function normalizeOIDCClaims(claims: Record): Record { + const normalized = { ...claims }; + + // Logto: username → preferred_username + if (!normalized.preferred_username && normalized.username) { + normalized.preferred_username = normalized.username; + } + + // Logto: name → family_name + given_name (split on last space) + if ((!normalized.family_name || !normalized.given_name) && normalized.name) { + const parts = normalized.name.trim().split(/\s+/); + if (parts.length >= 2) { + normalized.given_name = parts.slice(0, -1).join(' '); + normalized.family_name = parts[parts.length - 1]; + } else { + normalized.given_name = normalized.name; + normalized.family_name = normalized.name; + } + } + + return normalized; +} + export const OIDC = !building ? await makeOIDC({ development: dev, oidcAuthority: configPublic.PUBLIC_OIDC_AUTHORITY, oidcClientId: configPublic.PUBLIC_OIDC_CLIENT_ID, oidcClientSecret: configPrivate.OIDC_CLIENT_SECRET, + oidcScope: configPrivate.OIDC_SCOPES, loginCallbackRoute: configPublic.PUBLIC_OIDC_LOGIN_CALLBACK_ROUTE, logoutCallbackRoute: configPublic.PUBLIC_OIDC_LOGOUT_CALLBACK_ROUTE, - secret: configPrivate.SECRET, authenticatedRoutes: ['/app'], logoutPath: '', async userLoggedInSuccessfully({ user }) { + const normalized = normalizeOIDCClaims(user); await db .insert(schema.user) .values({ - id: user.sub, - locale: user.locale ?? configPublic.PUBLIC_DEFAULT_LOCALE, - preferredUsername: user.preferred_username!, - email: user.email!, - familyName: user.family_name!, - givenName: user.given_name! + id: normalized.sub, + locale: normalized.locale ?? configPublic.PUBLIC_DEFAULT_LOCALE, + preferredUsername: normalized.preferred_username ?? normalized.email, + email: normalized.email!, + familyName: normalized.family_name ?? '', + givenName: normalized.given_name ?? '' }) .onConflictDoUpdate({ target: schema.user.id, set: { - locale: user.locale ?? configPublic.PUBLIC_DEFAULT_LOCALE, - preferredUsername: user.preferred_username!, - email: user.email!, - familyName: user.family_name!, - givenName: user.given_name! + locale: normalized.locale ?? configPublic.PUBLIC_DEFAULT_LOCALE, + preferredUsername: normalized.preferred_username ?? normalized.email, + email: normalized.email!, + familyName: normalized.family_name ?? '', + givenName: normalized.given_name ?? '' } }); } diff --git a/src/api/services/isAdminEmail.ts b/src/api/services/isAdminEmail.ts new file mode 100644 index 00000000..f5ba1dfe --- /dev/null +++ b/src/api/services/isAdminEmail.ts @@ -0,0 +1,26 @@ +import { configPrivate } from '$config/private'; + +export function isAdminEmail(email: string) { + const whitelistEmails = configPrivate.ADMIN_EMAIL_WHITELIST.split(',').filter(Boolean); + const whitelistDomains = configPrivate.ADMIN_DOMAIN_WHITELIST.split(',').filter(Boolean); + const domain = email.split('@')[1]; + + return whitelistEmails.includes(email) || whitelistDomains.includes(domain); +} + +/** + * Check if the current user is a global admin (OIDC admin role OR whitelisted email). + * Global admins have full access to everything. + */ +export function isGlobalAdmin(ctx: { + hasRole: (role: string) => boolean; + mustBeLoggedIn: () => { email?: string | null }; +}): boolean { + if (ctx.hasRole('admin')) return true; + try { + const user = ctx.mustBeLoggedIn(); + return !!(user.email && isAdminEmail(user.email)); + } catch { + return false; + } +} diff --git a/src/api/services/isDMUNEmail.ts b/src/api/services/isDMUNEmail.ts deleted file mode 100644 index f8440232..00000000 --- a/src/api/services/isDMUNEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { configPrivate } from '$config/private'; - -export function isWhitelistedEmail(email: string) { - const whitelistEmails = configPrivate.ADMIN_EMAIL_WHITELIST.split(','); - const whitelistDomains = configPrivate.ADMIN_DOMAIN_WHITELIST.split(','); - const domain = email.split('@')[1]; - - return whitelistEmails.includes(email) || whitelistDomains.includes(domain); -} diff --git a/src/app.css b/src/app.css index 4715a3ec..9cd5ab03 100644 --- a/src/app.css +++ b/src/app.css @@ -2,6 +2,8 @@ @import '@deutschemodelunitednations/corporate-identity/css/shades/dmun'; @import 'tailwindcss'; +@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); +@import '@deutschemodelunitednations/munify-resolution-editor/tailwind.css'; @plugin '@tailwindcss/typography'; @plugin "daisyui"; diff --git a/src/client.ts b/src/client.ts index 8621441e..30cb5091 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,13 +1,26 @@ -import { HoudiniClient } from '$houdini'; +import { HoudiniClient, type ClientPlugin } from '$houdini'; import toast from 'svelte-french-toast'; import { error } from '@sveltejs/kit'; import { subscription } from '$houdini/plugins'; import { createClient } from 'graphql-sse'; +let redirecting = false; + +const authRedirect: ClientPlugin = () => ({ + end(ctx, { resolve, value }) { + if (!redirecting && value.errors?.some((e) => e.message === 'Must be logged in')) { + console.warn('[auth] Session expired, redirecting to login...'); + redirecting = true; + window.location.reload(); + } + resolve(ctx); + } +}); + const url = '/api/graphql'; export default new HoudiniClient({ url, - plugins: [subscription(() => createClient({ url }))], + plugins: [authRedirect, subscription(() => createClient({ url }))], throwOnError: { operations: ['mutation', 'subscription'], error: (errors, ctx) => { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c4fed324..5abe1549 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,9 +1,32 @@ -import type { Handle } from '@sveltejs/kit'; +import { type Handle, redirect } from '@sveltejs/kit'; import { paraglideMiddleware } from '$lib/paraglide/server'; import { sequence } from '@sveltejs/kit/hooks'; import { OIDC } from '$api/services/OIDC'; +import { locales, baseLocale, cookieName, cookieMaxAge } from '$lib/paraglide/runtime'; -export const handle: Handle = sequence(OIDC.handle, ({ event, resolve }) => +const nonBaseLocales = locales.filter((l) => l !== baseLocale); + +/** Redirect locale-prefixed URLs to bare paths, setting the cookie instead. */ +const localeRedirect: Handle = ({ event, resolve }) => { + const { pathname } = event.url; + for (const locale of nonBaseLocales) { + if (pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)) { + const bare = pathname.slice(`/${locale}`.length) || '/'; + const domain = event.url.hostname; + event.cookies.set(cookieName, locale, { + path: '/', + maxAge: cookieMaxAge, + domain, + httpOnly: false, + sameSite: 'lax' + }); + redirect(302, bare + event.url.search); + } + } + return resolve(event); +}; + +export const handle: Handle = sequence(OIDC.handle, localeRedirect, ({ event, resolve }) => paraglideMiddleware(event.request, ({ request: localizedRequest, locale }) => { event.request = localizedRequest; diff --git a/src/hooks.ts b/src/hooks.ts index f088616d..446fd89c 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,6 +1,2 @@ -import type { Reroute } from '@sveltejs/kit'; -import { deLocalizeUrl } from '$lib/paraglide/runtime'; - -export const reroute: Reroute = (request) => { - return deLocalizeUrl(request.url).pathname; -}; +// Reroute hook removed — locale is now cookie-based, not URL-based. +// Locale-prefixed URLs are redirected by the server hook. diff --git a/src/lib/components/AbbreviationInfoBox.svelte b/src/lib/components/AbbreviationInfoBox.svelte index 95dbeafb..b4a07dd6 100644 --- a/src/lib/components/AbbreviationInfoBox.svelte +++ b/src/lib/components/AbbreviationInfoBox.svelte @@ -24,8 +24,10 @@ }); -
-
+
+
{#if abbreviation}
{abbreviation}
{/if} diff --git a/src/lib/components/CommentSection.svelte b/src/lib/components/CommentSection.svelte new file mode 100644 index 00000000..5a0f019d --- /dev/null +++ b/src/lib/components/CommentSection.svelte @@ -0,0 +1,423 @@ + + +{#if !readonly || commentCount > 0} +
+ {#if marginIcon} + + + {:else} + + + {/if} + + {#if expanded} +
+ {#if topLevelComments.length === 0} +

{m.noComments()}

+ {/if} + + {#each topLevelComments as comment (comment.id)} +
+ +
+ {#if comment.author.committeeMember?.representation} + + {/if} + {getAuthorName(comment)} + {formatTime(comment.createdAt)} + {#if comment.visibility === 'PUBLIC'} + {m.publicComment()} + {/if} +
+ + + {#if editingId === comment.id} +
+ +
+ + +
+
+ {:else} +

{comment.content}

+ {/if} + + + {#if !readonly && editingId !== comment.id} +
+ + {#if isAuthor(comment)} + + + {/if} +
+ {/if} + + + {#if getReplies(comment.id).length > 0} +
+ {#each getReplies(comment.id) as reply (reply.id)} +
+
+ {#if reply.author.committeeMember?.representation} + + {/if} + {getAuthorName(reply)} + {formatTime(reply.createdAt)} + {#if reply.visibility === 'PUBLIC'} + {m.publicComment()} + {/if} +
+ + {#if editingId === reply.id} +
+ +
+ + +
+
+ {:else} +

{reply.content}

+ {/if} + + {#if !readonly && editingId !== reply.id && isAuthor(reply)} +
+ + +
+ {/if} +
+ {/each} +
+ {/if} + + + {#if !readonly && replyingTo === comment.id} +
+ +
+ {#if canPostTeamOnly} + + {/if} + +
+
+ {/if} +
+ {/each} + + + {#if !readonly} +
+ +
+ {#if canPostTeamOnly} + + {/if} + +
+
+ {/if} +
+ {/if} +
+{/if} diff --git a/src/lib/components/CommitteeGrid.svelte b/src/lib/components/CommitteeGrid.svelte index 46ce6cf3..b916c587 100644 --- a/src/lib/components/CommitteeGrid.svelte +++ b/src/lib/components/CommitteeGrid.svelte @@ -2,14 +2,19 @@ import * as m from '$lib/paraglide/messages.js'; import IconInfoBox from './IconInfoBox.svelte'; import { getCommitteeStatusIcon, getCommitteeStatusText } from '$lib/utils/committeeStatus'; - import { type CommitteeOverviewQuery$result, type MissionControlQuery$result } from '$houdini'; + import { + type CommitteeOverviewQuery$result, + type MissionControlQuery$result, + type ParticipantConferenceQuery$result + } from '$houdini'; import AdoptionConfetti from './AdoptionConfetti.svelte'; interface Props { conference: | MissionControlQuery$result['findFirstConference'] - | CommitteeOverviewQuery$result['findFirstConference']; - environment?: 'SPECTATOR' | 'TEAM'; + | CommitteeOverviewQuery$result['findFirstConference'] + | ParticipantConferenceQuery$result['findFirstConference']; + environment?: 'SPECTATOR' | 'TEAM' | 'PARTICIPANT'; } let { conference, environment = 'SPECTATOR' }: Props = $props(); @@ -17,6 +22,8 @@ const getHref = (committeeId: string) => { if (environment === 'TEAM') { return `/app/${conference.id}/${committeeId}/setup`; + } else if (environment === 'PARTICIPANT') { + return `/app/${conference.id}/participant/${committeeId}`; } else { return `/app/${conference.id}/${committeeId}`; } diff --git a/src/lib/components/CreateAmendmentModal.svelte b/src/lib/components/CreateAmendmentModal.svelte new file mode 100644 index 00000000..383ad1fb --- /dev/null +++ b/src/lib/components/CreateAmendmentModal.svelte @@ -0,0 +1,489 @@ + + + +
+

+ {editMode ? m.editAmendment() : isChairMode ? m.chairCreateAmendment() : m.proposeAmendment()} +

+ +
+ + {#if confirmingDelete} + +
+ +

{m.confirmDeleteAmendment()}

+

+ {m.amendmentDelete()} — OP {selectedSourceIndex + 1} +

+
+ + +
+
+ {:else} + +
    + {#if isChairMode} +
  • {m.selectProposerDelegation()}
  • + {/if} +
  • {m.selectAmendmentType()}
  • + {#if totalSteps > (isChairMode ? 2 : 1)} +
  • {m.edit()}
  • + {/if} +
+ + {#if isChairMode && step === 1} + + +
+ {#each filteredMembers as member (member.id)} + + {/each} + {#if filteredMembers.length === 0} +

{m.noResults()}

+ {/if} +
+ {:else if step === typeStep} + +
+ {#if editMode && selectedType} + + {@const typeBadgeClass = { + DELETE: 'badge-error', + ADD: 'badge-success', + ALTER_TEXT: 'badge-warning', + ALTER_POSITION: 'badge-info' + }[selectedType]} + {@const typeIcon = { + DELETE: 'fa-trash', + ADD: 'fa-plus', + ALTER_TEXT: 'fa-pen', + ALTER_POSITION: 'fa-arrows-alt' + }[selectedType]} + {@const typeLabel = { + DELETE: m.amendmentDelete(), + ADD: m.amendmentAdd(), + ALTER_TEXT: m.amendmentAlterText(), + ALTER_POSITION: m.amendmentAlterPosition() + }[selectedType]} +
+ + {typeLabel} +
+ {:else} +
+ + + + +
+ {/if} + + {#if selectedType} +
+ + {#if selectedType === 'ADD'} + + {:else if operativeClauses.length > 0} + + {:else} +

{m.noResults()}

+ {/if} +
+ +
+ {#if isChairMode} + + {/if} + +
+ {/if} +
+ {:else if step === contentStep} + +
+ {#if selectedType === 'ALTER_TEXT' || selectedType === 'ADD'} + {#if selectedType === 'ALTER_TEXT'} +

+ {m.alterText()} — OP {selectedSourceIndex + 1} +

+ {:else} +

+ {m.addClause()} — {m.targetPosition()}: + + {#if targetPosition === -1} + {m.insertAtBeginning()} + {:else} + {m.insertAfterPresentation({ index: String(targetPosition + 1) })} + {/if} + +

+ {/if} + {#if miniResolution} +
+ { + if (updated.operative[0]) { + newContent = updated.operative[0] as OperativeClause; + } + }} + /> +
+ {/if} + {:else if selectedType === 'ALTER_POSITION'} +

+ {m.alterPosition()} — OP {selectedSourceIndex + 1} +

+
+ + +
+ {/if} + +
+ + +
+
+ {/if} + {/if} +
diff --git a/src/lib/components/DeleteConferenceModal.svelte b/src/lib/components/DeleteConferenceModal.svelte new file mode 100644 index 00000000..b29c1e99 --- /dev/null +++ b/src/lib/components/DeleteConferenceModal.svelte @@ -0,0 +1,71 @@ + + + +

{m.deleteConference()}

+

{m.deleteConferenceWarning()}

+

{m.deleteConferenceConfirmation()}

+

{conferenceName}

+ + +
diff --git a/src/lib/components/Fieldset.svelte b/src/lib/components/Fieldset.svelte new file mode 100644 index 00000000..e2b3c007 --- /dev/null +++ b/src/lib/components/Fieldset.svelte @@ -0,0 +1,24 @@ + + +
+ {#if legend} + + {#if faIcon} + + {/if} + {legend} + + {/if} + {@render children()} +
diff --git a/src/lib/components/IconInfoBox.svelte b/src/lib/components/IconInfoBox.svelte index 9d8c5d9e..93123f4e 100644 --- a/src/lib/components/IconInfoBox.svelte +++ b/src/lib/components/IconInfoBox.svelte @@ -71,11 +71,11 @@
-
+
{#if iconText}
{iconText}
{:else if faIcon} diff --git a/src/lib/components/LanguageSwitcher.svelte b/src/lib/components/LanguageSwitcher.svelte index 7f2dcdba..5677230a 100644 --- a/src/lib/components/LanguageSwitcher.svelte +++ b/src/lib/components/LanguageSwitcher.svelte @@ -1,44 +1,40 @@ - + + +

+ {m.language()} +

+
+ {#each locales as l} + + {/each} +
+
diff --git a/src/lib/components/Majorities.svelte b/src/lib/components/Majorities.svelte index 8cd3f95d..7990bc69 100644 --- a/src/lib/components/Majorities.svelte +++ b/src/lib/components/Majorities.svelte @@ -1,4 +1,6 @@ - - - +{#if open} + + + +{/if} diff --git a/src/lib/components/voting/RollCallVotingChair.svelte b/src/lib/components/voting/RollCallVotingChair.svelte index 8f730f85..4340fda4 100644 --- a/src/lib/components/voting/RollCallVotingChair.svelte +++ b/src/lib/components/voting/RollCallVotingChair.svelte @@ -14,6 +14,7 @@ sortTranslatedCountries } from '$lib/utils/nationTranslationHelper.svelte'; import { calculateMajority } from '$lib/utils/majorities'; + import type { VotingResult } from './votingModal'; interface Props { active: boolean; @@ -21,13 +22,46 @@ voteName?: string; majority?: VotingMajority; withAbstentions?: boolean; + oncomplete?: (result: VotingResult) => void; } - let { active = $bindable(), committee, voteName, majority, withAbstentions }: Props = $props(); + let { + active = $bindable(), + committee, + voteName, + majority, + withAbstentions, + oncomplete + }: Props = $props(); let currentIndex = $state(0); let stage = $state<'ROLL_CALL' | 'EVALUATION'>('ROLL_CALL'); + const exitVote = (completed: boolean = false) => { + if (oncomplete) { + if (completed) { + const votesFor = rollCallVotingPro?.length ?? 0; + const votesAgainst = rollCallVotingCon?.length ?? 0; + const votesAbstain = rollCallVotingAbstain?.length ?? 0; + oncomplete({ + outcome: votesFor >= majorityAmount ? 'ADOPTED' : 'REJECTED', + votesFor, + votesAgainst, + votesAbstain, + cancelled: false + }); + } else { + oncomplete({ + votesFor: 0, + votesAgainst: 0, + votesAbstain: 0, + cancelled: true + }); + } + } + active = false; + }; + let members = committee?.members .filter((member) => member.present && member.representation?.type === 'DELEGATION') .sort((a, b) => sortTranslatedCountries(a.representation!, b.representation!)); @@ -162,7 +196,7 @@ } break; case 'esc': - active = false; + exitVote(stage === 'EVALUATION'); break; } }); @@ -270,7 +304,7 @@
diff --git a/src/lib/components/voting/VotingModal.svelte b/src/lib/components/voting/VotingModal.svelte new file mode 100644 index 00000000..a7d99511 --- /dev/null +++ b/src/lib/components/voting/VotingModal.svelte @@ -0,0 +1,145 @@ + + +{#if setupOpen} + +

{m.voting()}

+ +
+{/if} + +{#if phase === 'EXECUTING' && executingOpen} + {#if voteType === 'SHOW_OF_HANDS'} + + {:else} + + {/if} +{/if} diff --git a/src/lib/components/voting/VotingSetup.svelte b/src/lib/components/voting/VotingSetup.svelte index da6d1bdf..9e3101d9 100644 --- a/src/lib/components/voting/VotingSetup.svelte +++ b/src/lib/components/voting/VotingSetup.svelte @@ -1,10 +1,9 @@ -
-
- {m.typeOfVoting()} - (voteType = tab)} /> -
-
- {m.majoritySettings()} -

{m.majoritySettingsDescriptions()}

- (majority = tab)} /> - (withAbstentions = tab)} - /> -
-
- {m.voteTitel()} - -

{m.voteTitleDescription()}

-
- - -
+ { + if (voteType === 'SHOW_OF_HANDS') { + showOfHandModalOpen = true; + } else if (voteType === 'ROLL_CALL') { + rollCallModalOpen = true; + } + }} +/> + import type { VotingMajority } from '$lib/local-db/localDB'; + import { m } from '$lib/paraglide/messages'; + import Tabs from '../Tabs.svelte'; + + interface Props { + voteType: 'SHOW_OF_HANDS' | 'ROLL_CALL'; + voteName: string; + majority: VotingMajority; + withAbstentions: boolean; + onstart: () => void; + } + + let { + voteType = $bindable(), + voteName = $bindable(), + majority = $bindable(), + withAbstentions = $bindable(), + onstart + }: Props = $props(); + + const voteTypeTabs: { + id: 'SHOW_OF_HANDS' | 'ROLL_CALL'; + label: string; + faIcon: string; + }[] = [ + { id: 'SHOW_OF_HANDS', label: m.showOfHandsVoting(), faIcon: 'hand-wave' }, + { id: 'ROLL_CALL', label: m.rollCallVoting(), faIcon: 'list-check' } + ]; + + const majorityTabs: { + id: VotingMajority; + label: string; + }[] = [ + { id: 'SIMPLE', label: m.simpleMajority() }, + { id: 'ABSOLUTE', label: m.absoluteMajority() }, + { id: 'TWO_THIRDS', label: m.twoThirdsMajority() } + ]; + + const withAbstentionsTabs = [ + { id: false, label: m.withoutAbstentions() }, + { id: true, label: m.withAbstentions() } + ]; + + +
+
+ {m.typeOfVoting()} + (voteType = tab)} /> +
+
+ {m.majoritySettings()} +

{m.majoritySettingsDescriptions()}

+ (majority = tab)} /> + (withAbstentions = tab)} + /> +
+
+ {m.voteTitel()} + +

{m.voteTitleDescription()}

+
+ + +
diff --git a/src/lib/components/voting/votingModal.ts b/src/lib/components/voting/votingModal.ts new file mode 100644 index 00000000..5c22c6ba --- /dev/null +++ b/src/lib/components/voting/votingModal.ts @@ -0,0 +1,50 @@ +import { writable } from 'svelte/store'; +import type { VotingMajority } from '$lib/local-db/localDB'; + +export interface VotingConfig { + voteName?: string; + majority?: VotingMajority; + voteType?: 'SHOW_OF_HANDS' | 'ROLL_CALL'; + withAbstentions?: boolean; +} + +export interface VotingResult { + outcome?: 'ADOPTED' | 'REJECTED'; + votesFor: number; + votesAgainst: number; + votesAbstain: number; + cancelled: boolean; +} + +interface VotingModalState { + config: VotingConfig; + onComplete: (result: VotingResult) => void; +} + +export const votingModalStore = writable(null); + +export function openVotingModal(config: VotingConfig = {}): Promise { + return new Promise((resolve) => { + votingModalStore.set({ + config, + onComplete: (result) => { + votingModalStore.set(null); + resolve(result); + } + }); + }); +} + +export function closeVotingModal(): void { + votingModalStore.update((state) => { + if (state) { + state.onComplete({ + votesFor: 0, + votesAgainst: 0, + votesAbstain: 0, + cancelled: true + }); + } + return null; + }); +} diff --git a/src/lib/config/private.ts b/src/lib/config/private.ts index 9156c3b4..1c9243a4 100644 --- a/src/lib/config/private.ts +++ b/src/lib/config/private.ts @@ -1,17 +1,13 @@ import { env } from '$env/dynamic/private'; import { z } from 'zod'; import { getConfig } from './getConfig'; -import { nanoid } from 'nanoid'; const schema = z.object({ DATABASE_URL: z.string(), OIDC_CLIENT_SECRET: z.optional(z.string()), OIDC_SCOPES: z .string() - .default( - 'openid profile offline_access address email family_name gender given_name locale name phone preferred_username urn:zitadel:iam:org:projects:roles urn:zitadel:iam:user:metadata' - ), + .default('openid profile offline_access email phone identity role custom_data'), OIDC_ROLE_CLAIM: z.optional(z.string()), - SECRET: z.string().default(nanoid(50)), NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]), // TODO OTEL_SERVICE_NAME: z.string().default('MUNIFY-CHASE'), @@ -19,7 +15,8 @@ const schema = z.object({ OTEL_ENDPOINT_URL: z.optional(z.string()), OTEL_AUTHORIZATION_HEADER: z.optional(z.string()), ADMIN_EMAIL_WHITELIST: z.string().optional().default(''), - ADMIN_DOMAIN_WHITELIST: z.string().optional().default('') + ADMIN_DOMAIN_WHITELIST: z.string().optional().default(''), + REDIS_URL: z.string().optional() }); export const configPrivate = getConfig({ schema, envSource: env }); diff --git a/src/lib/data/presentationLayoutPresets.ts b/src/lib/data/presentationLayoutPresets.ts index 92ecd21d..c022746b 100644 --- a/src/lib/data/presentationLayoutPresets.ts +++ b/src/lib/data/presentationLayoutPresets.ts @@ -9,6 +9,7 @@ type PresentationLayoutPresets = { whiteboard?: ComponentProps; speakersList?: ComponentProps; commentsList?: ComponentProps; + resolutionPreview?: ComponentProps; }; const commonCommitteeTitleProps: Partial> = { @@ -60,7 +61,14 @@ const commonCommentsListProps: Partial> = { } }; -export type PresentationLayoutPresetOptions = 'default' | 'smallScreen'; +const commonResolutionPreviewProps: Partial> = { + min: { + w: 4, + h: 4 + } +}; + +export type PresentationLayoutPresetOptions = 'default' | 'smallScreen' | 'resolution'; const presentationLayoutPresets: Record< PresentationLayoutPresetOptions, @@ -117,6 +125,50 @@ const presentationLayoutPresets: Record< ...commonCommentsListProps } }, + resolution: { + committeeStatus: { + x: 0, + y: 0, + w: 4, + h: 2, + ...commonCommitteeStatusProps + }, + agendaItem: { + x: 4, + y: 0, + w: 4, + h: 2, + ...commonCommitteeAgendaItemProps + }, + majorities: { + x: 8, + y: 0, + w: 4, + h: 2, + ...commonMajoritiesProps + }, + resolutionPreview: { + x: 0, + y: 2, + w: 8, + h: 11, + ...commonResolutionPreviewProps + }, + speakersList: { + x: 8, + y: 2, + w: 4, + h: 5, + ...commonSpeakersListProps + }, + commentsList: { + x: 8, + y: 7, + w: 4, + h: 6, + ...commonCommentsListProps + } + }, smallScreen: { committeeStatus: { x: 0, diff --git a/src/lib/local-db/localDB.ts b/src/lib/local-db/localDB.ts index 9428153b..a0573fff 100644 --- a/src/lib/local-db/localDB.ts +++ b/src/lib/local-db/localDB.ts @@ -8,6 +8,7 @@ interface CommitteeSettings { committeeId: string; layout: PresentationLayoutPresetOptions; presentationRootFontSize: number; + presentationResolutionFontSize: number; displayRegionalGroups: boolean; rollCall: number | null; @@ -38,6 +39,7 @@ localDB.version(1).stores({ ++committeeId, layout, presentationRootFontSize, + presentationResolutionFontSize, displayRegionalGroups, rollCall, currentVoting, diff --git a/src/lib/utils/import.ts b/src/lib/utils/import.ts index 1dfe3da4..af3d1a51 100644 --- a/src/lib/utils/import.ts +++ b/src/lib/utils/import.ts @@ -8,7 +8,8 @@ export const importDataSchema = z.object({ z.object({ id: z.string(), name: z.string(), - abbreviation: z.string() + abbreviation: z.string(), + resolutionHeadline: z.string().optional() }) ), representations: z.array( diff --git a/src/lib/utils/nationTranslationHelper.svelte.ts b/src/lib/utils/nationTranslationHelper.svelte.ts index 609b1d97..156c2ed2 100644 --- a/src/lib/utils/nationTranslationHelper.svelte.ts +++ b/src/lib/utils/nationTranslationHelper.svelte.ts @@ -251,6 +251,8 @@ function nationCodeToLocalName(code: string, locale = getLocale(), official = fa return 'deu'; case 'en': return 'eng'; + case 'pt': + return 'por'; default: return 'eng'; } diff --git a/src/lib/utils/paperNameGenerator.ts b/src/lib/utils/paperNameGenerator.ts new file mode 100644 index 00000000..16e4ab8c --- /dev/null +++ b/src/lib/utils/paperNameGenerator.ts @@ -0,0 +1,145 @@ +import { getLocale } from '$lib/paraglide/runtime'; + +const adverbs = { + en: [ + 'Very', + 'Super', + 'Ultra', + 'Quite', + 'Totally', + 'Absolutely', + 'Fairly', + 'Really', + 'Extremely', + 'Incredibly', + 'Remarkably', + 'Exceptionally', + 'Tremendously', + 'Hugely', + 'Fantastically' + ], + de: [ + 'Sehr', + 'Super', + 'Ultra', + 'Ziemlich', + 'Total', + 'Absolut', + 'Recht', + 'Wirklich', + 'Extrem', + 'Unglaublich', + 'Bemerkenswert', + 'Außergewöhnlich', + 'Enorm', + 'Riesig', + 'Fantastisch' + ], + pt: [ + 'Muito', + 'Super', + 'Ultra', + 'Bastante', + 'Totalmente', + 'Absolutamente', + 'Razoavelmente', + 'Realmente', + 'Extremamente', + 'Incrivelmente', + 'Notavelmente', + 'Excepcionalmente', + 'Tremendamente', + 'Imensamente', + 'Fantasticamente' + ] +}; + +const adjectives = { + en: [ + 'Happy', + 'Calm', + 'Excited', + 'Energetic', + 'Hopeful', + 'Content', + 'Curious', + 'Motivated', + 'Cheerful', + 'Determined', + 'Confident', + 'Magnificent', + 'Grand', + 'Majestic', + 'Splendid', + 'Glorious', + 'Noble', + 'Dignified', + 'Optimistic' + ], + de: [ + 'Fröhlicher', + 'Ruhiger', + 'Begeisterter', + 'Energischer', + 'Hoffnungsvoller', + 'Zufriedener', + 'Neugieriger', + 'Motivierter', + 'Heiterer', + 'Entschlossener', + 'Selbstbewusster', + 'Großartiger', + 'Grandioser', + 'Majestätischer', + 'Prächtiger', + 'Glorreicher', + 'Edler', + 'Würdevoller', + 'Optimistischer' + ], + pt: [ + 'Feliz', + 'Calmo', + 'Entusiasmado', + 'Energético', + 'Esperançoso', + 'Contente', + 'Curioso', + 'Motivado', + 'Alegre', + 'Determinado', + 'Confiante', + 'Magnífico', + 'Grandioso', + 'Majestoso', + 'Esplêndido', + 'Glorioso', + 'Nobre', + 'Digno', + 'Otimista' + ] +}; + +const unSecretaryGenerals = [ + 'Trygve Lie', + 'Dag Hammarskjöld', + 'U Thant', + 'Kurt Waldheim', + 'Javier Pérez de Cuéllar', + 'Boutros Boutros-Ghali', + 'Kofi Annan', + 'Ban Ki-moon', + 'António Guterres' +]; + +function pick(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +export function generatePaperName(): string { + const locale = getLocale() as 'en' | 'de' | 'pt'; + const adverbList = adverbs[locale] ?? adverbs.en; + const adjectiveList = adjectives[locale] ?? adjectives.en; + + return `${pick(adverbList)} ${pick(adjectiveList)} ${pick(unSecretaryGenerals)}`; +} diff --git a/src/lib/utils/resolutionEditorLabels.ts b/src/lib/utils/resolutionEditorLabels.ts new file mode 100644 index 00000000..017e3fe0 --- /dev/null +++ b/src/lib/utils/resolutionEditorLabels.ts @@ -0,0 +1,132 @@ +/** + * i18n Adapter for the Resolution Editor Library + * + * Maps Paraglide messages to the library's ResolutionEditorLabels interface. + */ + +import type { ResolutionEditorLabels } from '@deutschemodelunitednations/munify-resolution-editor/i18n'; +import * as m from '$lib/paraglide/messages'; + +/** + * Creates a ResolutionEditorLabels object from Paraglide messages. + * Call this function to get the current language's labels. + */ +export function getResolutionLabels(): ResolutionEditorLabels { + return { + // Editor chrome + resolutionEditor: m.resolutionEditor(), + resolution: m.resolution(), + resolutionPreview: m.resolutionPreview(), + resolutionShowPreview: m.resolutionShowPreview(), + resolutionHidePreview: m.resolutionHidePreview(), + + // Sections + resolutionCommittee: m.resolutionCommittee(), + resolutionPreambleClauses: m.resolutionPreambleClauses(), + resolutionOperativeClauses: m.resolutionOperativeClauses(), + resolutionSubClauses: m.resolutionSubClauses(), + + // Actions + resolutionAddClause: m.resolutionAddClause(), + resolutionAddFirstClause: m.resolutionAddFirstClause(), + resolutionDeleteClause: m.resolutionDeleteClause(), + resolutionDeleteBlock: m.resolutionDeleteBlock(), + resolutionMoveUp: m.resolutionMoveUp(), + resolutionMoveDown: m.resolutionMoveDown(), + resolutionIndent: m.resolutionIndent(), + resolutionOutdent: m.resolutionOutdent(), + resolutionAddSubClause: m.resolutionAddSubClause(), + resolutionAddSibling: m.resolutionAddSibling(), + resolutionAddNested: m.resolutionAddNested(), + resolutionAddContinuation: m.resolutionAddContinuation(), + + // Placeholders + resolutionPreamblePlaceholder: m.resolutionPreamblePlaceholder(), + resolutionOperativePlaceholder: m.resolutionOperativePlaceholder(), + resolutionSubClausePlaceholder: m.resolutionSubClausePlaceholder(), + resolutionContinuationPlaceholder: m.resolutionContinuationPlaceholder(), + + // Empty states + resolutionNoPreambleClauses: m.resolutionNoPreambleClauses(), + resolutionNoOperativeClauses: m.resolutionNoOperativeClauses(), + resolutionNoClausesYet: m.resolutionNoClausesYet(), + + // Validation + resolutionUnknownPhrase: m.resolutionUnknownPhrase(), + + // Phrase lookup + phraseLookup: m.phraseLookup(), + phraseLookupTitle: m.phraseLookupTitle(), + phraseLookupSearch: m.phraseLookupSearch(), + phraseLookupDisclaimer: m.phraseLookupDisclaimer(), + phraseLookupNoResults: m.phraseLookupNoResults(), + phraseCopied: m.phraseCopied(), + copyFailed: m.copyFailed(), + + // Import - {count} interpolation is handled by the library + resolutionImport: m.resolutionImport(), + resolutionImportPreamble: m.resolutionImportPreamble(), + resolutionImportOperative: m.resolutionImportOperative(), + resolutionImportButton: m.resolutionImportButton({ count: '{count}' }), + resolutionImportPreview: m.resolutionImportPreview({ count: '{count}' }), + resolutionImportHintPreamble: m.resolutionImportHintPreamble(), + resolutionImportHintOperative: m.resolutionImportHintOperative(), + resolutionImportTipsTitle: m.resolutionImportTipsTitle(), + resolutionImportTipsPreamble1: m.resolutionImportTipsPreamble1(), + resolutionImportTipsPreamble2: m.resolutionImportTipsPreamble2(), + resolutionImportTipsPreamble3: m.resolutionImportTipsPreamble3(), + resolutionImportTipsOperative1: m.resolutionImportTipsOperative1(), + resolutionImportTipsOperative2: m.resolutionImportTipsOperative2(), + resolutionImportTipsOperative3: m.resolutionImportTipsOperative3(), + resolutionImportTipsOperative4: m.resolutionImportTipsOperative4(), + resolutionImportLLMTitle: m.resolutionImportLLMTitle(), + resolutionImportLLMInstructions: m.resolutionImportLLMInstructions(), + resolutionImportLLMCopyPrompt: m.resolutionImportLLMCopyPrompt(), + resolutionImportLLMCopied: m.resolutionImportLLMCopied(), + resolutionImportLLMPromptPreamble: m.resolutionImportLLMPromptPreamble(), + resolutionImportLLMPromptOperative: m.resolutionImportLLMPromptOperative(), + + // Preview metadata + resolutionSponsoringDelegations: m.resolutionSponsoringDelegations(), + resolutionAuthoringDelegation: m.resolutionAuthoringDelegation(), + resolutionDisclaimer: m.resolutionDisclaimer({ conferenceName: '{conferenceName}' }), + + // Locking + startEditing: m.startEditing(), + doneEditing: m.doneEditing(), + + // Amendments + amendmentProposed: m.amendmentProposed(), + amendmentAdd: m.amendmentAdd(), + amendmentDelete: m.amendmentDelete(), + amendmentAlterText: m.amendmentAlterText(), + amendmentAlterPosition: m.amendmentAlterPosition(), + amendmentRejectedClause: m.amendmentRejectedClause(), + + // Common + close: m.close(), + cancel: m.cancel(), + copy: m.copy() + }; +} + +/** + * Creates localized import button text with count interpolation. + */ +export function getImportButtonLabel(count: number): string { + return m.resolutionImportButton({ count: count.toString() }); +} + +/** + * Creates localized import preview text with count interpolation. + */ +export function getImportPreviewLabel(count: number): string { + return m.resolutionImportPreview({ count: count.toString() }); +} + +/** + * Creates localized disclaimer text with conference name interpolation. + */ +export function getDisclaimerText(conferenceName: string): string { + return m.resolutionDisclaimer({ conferenceName }); +} diff --git a/src/routes/(pages)/Card.svelte b/src/routes/(pages)/Card.svelte index 906315da..ab2e9aae 100644 --- a/src/routes/(pages)/Card.svelte +++ b/src/routes/(pages)/Card.svelte @@ -12,9 +12,11 @@ let { src, alt, header, text, comingSoonRibbon = false }: Props = $props(); -
+
- +

diff --git a/src/routes/(pages)/CardSection.svelte b/src/routes/(pages)/CardSection.svelte index d9340872..2e0da9be 100644 --- a/src/routes/(pages)/CardSection.svelte +++ b/src/routes/(pages)/CardSection.svelte @@ -31,7 +31,6 @@ alt="Resolution Editor" header={m.homeHeroCardResolutionEditorTitle()} text={m.homeHeroCardResolutionEditorText()} - comingSoonRibbon />

diff --git a/src/routes/(pages)/ContactSection.svelte b/src/routes/(pages)/ContactSection.svelte index 01bd4060..536f8050 100644 --- a/src/routes/(pages)/ContactSection.svelte +++ b/src/routes/(pages)/ContactSection.svelte @@ -8,13 +8,11 @@ class="flex flex-col items-center gap-6 mx-4 p-4 py-20 lg:p-20 bg-base-100 rounded-box border-base-300 border shadow-lg" >

{m.homeContactTitle()}

-

+

{m.homeContactText()}

diff --git a/src/routes/(pages)/LandingHero.svelte b/src/routes/(pages)/LandingHero.svelte index 9aa77f11..b7cd3737 100644 --- a/src/routes/(pages)/LandingHero.svelte +++ b/src/routes/(pages)/LandingHero.svelte @@ -39,7 +39,7 @@ class="mb-4 text-center font-serif text-5xl leading-tight font-bold lg:text-right lg:text-6xl" > MUN @@ -47,9 +47,7 @@
-

+

{m.homeHeroText()}

diff --git a/src/routes/(pages)/TextSection.svelte b/src/routes/(pages)/TextSection.svelte index cc590d75..cbbcb718 100644 --- a/src/routes/(pages)/TextSection.svelte +++ b/src/routes/(pages)/TextSection.svelte @@ -11,14 +11,12 @@

{title}

-

+

{text}

{#if children} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 80600bf5..478c9e1e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,5 @@ @@ -81,7 +94,7 @@ {#each conferenceData as c} {@const conf = c.conference}
@@ -97,5 +110,49 @@
+ {#if isGlobalAdmin} +
+ + + {#if manageMode} +
+
+
+ {#each allConferences as conf} +
+ {conf.title} + +
+ {/each} +
+
+
+ {/if} +
+ {/if} +
+ +{#if deleteTarget} + +{/if} diff --git a/src/routes/app/(launcher)/+page.ts b/src/routes/app/(launcher)/+page.ts index cad4c342..e383f5d5 100644 --- a/src/routes/app/(launcher)/+page.ts +++ b/src/routes/app/(launcher)/+page.ts @@ -6,11 +6,20 @@ export const _houdini_load = graphql(` findManyConferenceUser(where: { user: { id: $userId } }) { id conferenceUserType + committeeMemberId + committeeMember { + committeeId + } conference { id title } } + isGlobalAdmin + findManyConference { + id + title + } } `); diff --git a/src/routes/app/(launcher)/import/+page.svelte b/src/routes/app/(launcher)/import/+page.svelte index cce0bd64..6aab1f29 100644 --- a/src/routes/app/(launcher)/import/+page.svelte +++ b/src/routes/app/(launcher)/import/+page.svelte @@ -1,7 +1,7 @@ + + + + diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts index e01275b9..ded0a1fe 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/committeeSubscription.ts @@ -6,6 +6,7 @@ export const CommitteeSubscription = graphql(` id abbreviation name + resolutionHeadline stateOfDebate status statusHeadline @@ -14,8 +15,17 @@ export const CommitteeSubscription = graphql(` simpleMajority twoThirdsMajority paperSupportThreshold + maxDraftResolutions + activeDraftResolutionId + supportReEvaluationOpen + amendmentSubmissionOpen + amendmentSponsoringOpen + currentOperativeIndex + currentOperativeClauseId + activeAmendmentId whiteboardContent lastResolutionAdoptionDate + allowDelegationsToAddThemselvesToSpeakersList activeAgendaItem { id title @@ -76,7 +86,9 @@ export const CommitteeSubscription = graphql(` } } conference { + title hasModeratedCaucus + resolutionFeatureEnabled uniqueConferenceMembers { id representation { diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.svelte b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.svelte new file mode 100644 index 00000000..9a90b05e --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.svelte @@ -0,0 +1,695 @@ + + + + {m.resolutions()} - MUNify CHASE + + +{#if committee} +
+
+ +
+ + + + + + +
+ + +
+ + {#if committee.activeAgendaItem} +
+ +
+ {/if} + + + +

{m.submittedPapersDescription()}

+ + {#if submittedPapers.length === 0} +
+ {m.noSubmittedPapers()} +
+ {:else} +
+ {#each submittedPapers as paper, i (paper.id)} +
+
+
+
+ {#if i < availableSlots} + {m.topCandidate()} + {/if} +

+ {paper.title || m.untitledPaper()} +

+
+
+ {#if paper.creator?.representation} + + + {paper.creator.representation.name} + + {/if} + + + {m.sponsorCount({ + count: String(paper.sponsors.length) + })} + + {timeAgo(paper.updatedAt)} +
+
+
+ + {m.viewPaper()} + + +
+
+
+ {/each} +
+ {/if} +
+ + + + {#if draftResolutions.length === 0} +
+ {m.noDraftResolutionsYet()} +
+ {:else} +
+ {#each draftResolutions as paper (paper.id)} + {@const isActive = paper.id === committee.activeDraftResolutionId} + {@const canSetActive = + !isActive && + (paper.status === 'DRAFT_RESOLUTION' || + paper.status === 'AMENDMENT_PHASE' || + paper.status === 'VOTING_PHASE')} + + {/each} +
+ {/if} +
+ + + +
+ +
+
+
+

{m.supportReEvaluation()}

+

+ {#if committee.supportReEvaluationOpen} + {m.supportReEvaluationOpen()} + {:else} + {m.supportReEvaluationClosed()} + {/if} +

+
+ toggleReEvaluation(!committee.supportReEvaluationOpen)} + /> +
+
+ + +
+
+
+

{m.amendmentSubmission()}

+

+ {#if committee.amendmentSubmissionOpen} + {m.amendmentSubmissionOpen()} + {:else} + {m.amendmentSubmissionClosed()} + {/if} +

+
+ toggleAmendmentSubmission(!committee.amendmentSubmissionOpen)} + /> +
+
+ + +
+
+
+

{m.amendmentSponsoring()}

+

+ {#if committee.amendmentSponsoringOpen} + {m.amendmentSponsoringOpen()} + {:else} + {m.amendmentSponsoringClosed()} + {/if} +

+
+ toggleAmendmentSponsoring(!committee.amendmentSponsoringOpen)} + /> +
+
+ + {#if canStartAmendmentPhase} +
+
+
+ {activeDr!.documentNumber} — {m.draftResolution()} +
+ +
+ {:else if isInVotingPhase} +
+
+
+ {m.votingPhaseActive()} + {activeDr!.documentNumber} +
+ + {m.goToVoting()} → + +
+ {:else if isInAmendmentPhase} +
+
+
+ {m.amendmentPhaseActive()} + OP {(committee.currentOperativeIndex ?? 0) + 1} +
+ + {m.goToAmendments()} → + +
+ {/if} +
+
+ + + + {#if isInVotingPhase} +
+
+ {m.votingPhaseActive()} + {activeDr!.documentNumber} +
+ + {m.goToVoting()} → + +
+ {:else if isInAmendmentPhase} +
+ + {m.finishAmendmentPhaseFirst()} +
+ {:else if activeDr} +
+ + {m.finishAmendmentPhaseFirst()} +
+ {:else} +
+

{m.noActiveDrForVoting()}

+
+ {/if} +
+
+
+
+ + + +
+

{m.promoteToDraftResolution()}

+

{m.promoteToDraftResolutionConfirm()}

+

{promotePaperTitle}

+
+ + +
+
+
+ + + +
+

{m.startAmendmentPhase()}

+

{m.confirmStartAmendmentPhase()}

+
+ + +
+
+
+ + + +
+

{m.chairCreateWorkingPaper()}

+ +
+

{m.selectAuthorDelegation()}

+ +
+ {#each filteredCreatePaperMembers as member (member.id)} + + {/each} + {#if filteredCreatePaperMembers.length === 0} +

{m.noResults()}

+ {/if} +
+
+{/if} diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.ts new file mode 100644 index 00000000..967d74ea --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/+page.ts @@ -0,0 +1,49 @@ +import { graphql } from '$houdini'; +import type { ChairResolutionPapersQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ChairResolutionPapersQuery($committeeId: ID!) { + findManyResolutionPaper(where: { committeeId: $committeeId }) { + id + title + status + documentNumber + sequenceNumber + updatedAt + creatorCommitteeMemberId + agendaItem { + id + title + } + creator { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + } +`); + +export const _ChairResolutionPapersQueryVariables: ChairResolutionPapersQueryVariables = ( + event +) => { + return { + committeeId: event.params.committeeId + }; +}; diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.svelte b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.svelte new file mode 100644 index 00000000..f8ad2598 --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.svelte @@ -0,0 +1,2430 @@ + + + + {paper?.documentNumber ?? m.draftResolution()} - MUNify CHASE + + +{#if paper} +
+ +
+ + + {m.backToResolutions()} + +
+ {#if saveStatus === 'saving'} + + {m.savingChanges()} + + {:else if saveStatus === 'saved'} + + {m.changesSaved()} + + {:else if saveStatus === 'error'} + + {m.saveError()} + + {/if} + + + +
+
+ + +
+ +
+
+ + {paper.documentNumber ?? m.draftResolution()} + + + {getStatusText(paper.status)} + +
+
+
+ + {#if paper.agendaItem} +
+ {m.agendaItem()}: + {paper.agendaItem.title} +
+ {/if} + + + {#if paper.creator?.representation} +
+ {m.submittingNation()}: + + {paper.creator.representation.name ?? + getTranslatedCountryNameFromAlpha3Code(paper.creator.representation.alpha3Code)} +
+ {/if} + + +
+
+ {#each sortedSponsors as sponsor (sponsor.id)} +
+ + +
+ {/each} + +
+

+ {m.sponsorCount({ count: String(paper.sponsors.length) })} +

+
+ + {#if paper.status !== 'WORKING_PAPER'} + + {/if} +
+
+ + + {#if hasOtherLocks} +
+ + {m.collaborativeEditingInfo()} +
+ {/if} + + + {#if paper.status === 'FINAL' && voteResult} +
+ +
+ + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + + + {m.votesFor()}: {voteResult.votesFor} | {m.votesAgainst()}: {voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0} + | {m.votesAbstain()}: {voteResult.votesAbstain} + {/if} + +
+
+ {/if} + + + {#if allComments.length > 0} +
+
+ + {allComments.length} + {m.comments()} +
+ | +
+ {documentCommentCount} + {m.documentWide()} +
+ | +
+ {clauseCommentCount} + {m.clauseComments()} +
+
+ {/if} + + + {#if canEdit} +
+ +
+ {/if} + + +
+ {#if resolution} + + {#snippet preambleAnnotations({ clause })} + {@const lock = locksByClauseId.get(clause.id)} + {@const commentCount = commentCountByClauseId.get(clause.id) ?? 0} + {#if lock} +
+
+ {#if lock.conferenceUser?.committeeMember?.representation} + + {/if} + +
+
+ {/if} + {#if commentCount > 0} +
+ + {commentCount} +
+ {/if} + {/snippet} + {#snippet clauseAnnotations({ clause })} + {@const lock = locksByClauseId.get(clause.id)} + {@const commentCount = commentCountByClauseId.get(clause.id) ?? 0} + {#if lock} +
+
+ {#if lock.conferenceUser?.committeeMember?.representation} + + {/if} + +
+
+ {/if} + {#if commentCount > 0} +
+ + {commentCount} +
+ {/if} + {/snippet} + {#snippet preambleClauseToolbar({ clause })} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/snippet} + {#snippet clauseToolbar({ clause })} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/snippet} + {#snippet afterPreambleClause({ clause })} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/snippet} + {#snippet afterOperativeClause({ clause })} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/snippet} +
+ {/if} +
+ + + {#if paper.status === 'AMENDMENT_PHASE'} +
+
+ + OP {currentOpIndex + 1} / {operativeClauses.length} + +
+ + +
+
+
+ +
+
+ +
+ + {#if sortedSubmittedAmendments.length === 0} +

{m.noAmendments()}

+ {:else} +
+ {#each groupedAmendments as group} +
+

+ {group.label} +

+
+ {#each group.amendments as amendment (amendment.id)} + {@const isActive = amendment.id === activeAmendmentId} + {@const sponsorCount = amendment.sponsors?.length ?? 0} + {@const thresholdMet = sponsorCount >= sponsorThresholdNeeded} +
+
+
+ + {amendment.documentNumber ?? getAmendmentTypeLabel(amendment.type)} + + {#if amendment.proposer?.representation} +
+ + + {amendment.proposer.representation.name ?? + getTranslatedCountryNameFromAlpha3Code( + amendment.proposer.representation.alpha3Code + )} + +
+ {/if} + + {sponsorCount}/{sponsorThresholdNeeded} + + {#if isActive} + {m.activeAmendment()} + {/if} +
+ + + {#if amendment.type === 'ALTER_TEXT' && amendment.newContent} +
+ + {getFirstTextContent(amendment.newContent as OperativeClause).slice( + 0, + 120 + )}{getFirstTextContent(amendment.newContent as OperativeClause) + .length > 120 + ? '…' + : ''} + +
+ {:else if amendment.type === 'ALTER_POSITION' && amendment.targetPosition != null} +
+ + {#if amendment.targetPosition === -1} + {m.insertAtBeginning()} + {:else} + {m.insertAfterPresentation({ + index: String(amendment.targetPosition + 1) + })} + {/if} +
+ {:else if amendment.type === 'ADD' && amendment.newContent} +
+
+ + + {#if amendment.targetPosition === -1} + {m.insertAtBeginning()} + {:else if amendment.targetPosition != null} + {m.insertAfterPresentation({ + index: String(amendment.targetPosition + 1) + })} + {/if} + + + {getFirstTextContent(amendment.newContent as OperativeClause).slice( + 0, + 120 + )}{getFirstTextContent(amendment.newContent as OperativeClause) + .length > 120 + ? '…' + : ''} + +
+
+ {/if} + + +
+ {#each amendment.sponsors ?? [] as sponsor (sponsor.id)} +
+ + +
+ {/each} + +
+ + +
+
+ + +
+
+ + + + +
+
+
+
+ {/each} +
+
+ {/each} +
+ {/if} +
+ + +
+ +
+ {/if} + + + {#if paper.status === 'VOTING_PHASE'} + +
+
+ + OP {currentOpIndex + 1} / {operativeClauses.length} + + + {m.clausesVoted({ + voted: String(votedClauseCount), + total: String(operativeClauses.length) + })} + +
+ + +
+ + +
+ + + {@const currentClause = operativeClauses[currentOpIndex]} + {#if currentClause} + {@const existingVote = clauseVoteMap.get(currentClause.id)} + {#if existingVote} + +
+ + + OP {currentOpIndex + 1}: + + {existingVote.outcome === 'ADOPTED' ? m.adopted() : m.rejected()} + + — {m.votesFor()}: {existingVote.votesFor} | {m.votesAgainst()}: {existingVote.votesAgainst} + {#if existingVote.votesAbstain > 0} + | {m.votesAbstain()}: {existingVote.votesAbstain} + {/if} + + +
+ {:else} + +
+

+ {m.voteOnParagraph({ index: String(currentOpIndex + 1) })} +

+ +
+ {/if} + {/if} +
+ + +
+
+ {#each operativeClauses as clause, i (clause.id)} + {@const vote = clauseVoteMap.get(clause.id)} + + {/each} +
+
+ + +
+ {#if voteResult} +
+ + + + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + + — {m.votesFor()}: {voteResult.votesFor} | {m.votesAgainst()}: {voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0} + | {m.votesAbstain()}: {voteResult.votesAbstain} + {/if} + +
+ {:else} +

{m.finalVoteDescription()}

+ + {/if} +
+ {/if} + + + {#if paper.status === 'FINAL' && clauseVotes.length > 0} +
+
+ {#each operativeClauses as clause, i (clause.id)} + {@const vote = clauseVoteMap.get(clause.id)} +
+ OP {i + 1} + {#if vote} + + {vote.outcome === 'ADOPTED' ? m.adopted() : m.rejected()} + + + {vote.votesFor}/{vote.votesAgainst} + {#if vote.votesAbstain > 0}/{vote.votesAbstain}{/if} + + {:else} + + {/if} +
+ {/each} + {#if voteResult} +
+
+ {m.finalVote()} + + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + + + {voteResult.votesFor}/{voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0}/{voteResult.votesAbstain}{/if} + +
+ {/if} +
+
+ {/if} + + +
+ + onCreateComment(content, visibility, parentCommentId, null)} + {onUpdateComment} + {onDeleteComment} + /> +
+
+ + + +
+

{m.addSponsor()}

+ +
+ +
+ {#each filteredAvailableMembers as member (member.id)} + + {/each} + {#if filteredAvailableMembers.length === 0} +

{m.noResults()}

+ {/if} +
+
+ + + +
+

{m.addSponsor()}

+ +
+ +
+ {#each filteredAvailableAmendmentMembers as member (member.id)} + + {/each} + {#if filteredAvailableAmendmentMembers.length === 0} +

{m.noResults()}

+ {/if} +
+
+ + + +
+

{m.adoptByConsensus()}

+

{m.confirmAdoptByConsensus()}

+
+ + +
+
+
+ + + +
+

{m.amendmentRejected()}

+

{m.confirmRejectAmendment()}

+
+ + +
+
+
+ + + +
+

{m.voteOutcome()}

+
+ + + +
+
+
+ + + +
+

{m.startVotingPhase()}

+

{m.confirmStartVotingPhase()}

+
+ + +
+
+
+ + + +
+

{m.voteOutcome()}

+
+ + + +
+
+
+ + + +
+

{m.voteOutcome()}

+
+ + + + +
+
+
+ + + +
+

{m.revertStatus()}

+

+ {m.confirmRevertStatus({ + from: getStatusText(paper.status), + to: getStatusText(getPreviousStatus(paper.status)) + })} +

+ + {#if paper.status === 'AMENDMENT_PHASE'} + + {/if} + + {#if paper.status === 'VOTING_PHASE'} +
+ + {m.revertVotingWarning()} +
+ {/if} + + {#if paper.status === 'DRAFT_RESOLUTION'} +
+ + {m.revertDrWarning()} +
+ {/if} + +
+ + +
+
+
+ + + + + + {#if editingAmendment} + + {/if} +{/if} diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.ts new file mode 100644 index 00000000..e011e60b --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/+page.ts @@ -0,0 +1,56 @@ +import { graphql } from '$houdini'; +import type { ChairPaperDetailQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ChairPaperDetailQuery($paperId: ID!, $conferenceId: ID!, $userId: ID!) { + findFirstResolutionPaper(where: { id: $paperId }) { + id + title + status + content + documentNumber + sequenceNumber + updatedAt + agendaItem { + id + title + } + creator { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + currentUser: findManyConferenceUser( + where: { conferenceId: $conferenceId, user: { id: $userId } } + limit: 1 + ) { + id + } + } +`); + +export const _ChairPaperDetailQueryVariables: ChairPaperDetailQueryVariables = async (event) => { + const { user } = await event.parent(); + return { + paperId: event.params.paperId, + conferenceId: event.params.conferenceId, + userId: user.sub + }; +}; diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairAmendmentsSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairAmendmentsSubscription.ts new file mode 100644 index 00000000..78a63a4b --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairAmendmentsSubscription.ts @@ -0,0 +1,40 @@ +import { graphql } from '$houdini'; + +export const ChairAmendmentsSubscription = graphql(` + subscription ChairAmendmentsSubscription($paperId: ID!) { + findManyAmendment(where: { paperId: $paperId }) { + id + type + status + documentNumber + targetClauseId + targetOperativeIndex + targetPosition + newContent + proposerCommitteeMemberId + createdAt + proposer { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairClauseVotesSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairClauseVotesSubscription.ts new file mode 100644 index 00000000..b7319201 --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairClauseVotesSubscription.ts @@ -0,0 +1,14 @@ +import { graphql } from '$houdini'; + +export const ChairClauseVotesSubscription = graphql(` + subscription ChairClauseVotesSubscription($paperId: ID!) { + findManyOperativeClauseVote(where: { paperId: $paperId }) { + id + clauseId + outcome + votesFor + votesAgainst + votesAbstain + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairCommentsSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairCommentsSubscription.ts new file mode 100644 index 00000000..93748b3f --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairCommentsSubscription.ts @@ -0,0 +1,31 @@ +import { graphql } from '$houdini'; + +export const ChairPaperCommentsSubscription = graphql(` + subscription ChairPaperCommentsSubscription($paperId: ID!) { + findManyResolutionComment(where: { paperId: $paperId }) { + id + clauseId + content + visibility + parentCommentId + createdAt + updatedAt + author { + id + user { + givenName + familyName + } + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + conferenceUserType + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairLockSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairLockSubscription.ts new file mode 100644 index 00000000..840c111b --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairLockSubscription.ts @@ -0,0 +1,22 @@ +import { graphql } from '$houdini'; + +export const ChairPaperClauseLocksSubscription = graphql(` + subscription ChairPaperClauseLocksSubscription($paperId: ID!) { + findManyPaperClauseLock(where: { paperId: $paperId }) { + id + clauseId + conferenceUserId + acquiredAt + conferenceUser { + committeeMember { + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairPaperDetailSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairPaperDetailSubscription.ts new file mode 100644 index 00000000..3a12d89f --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairPaperDetailSubscription.ts @@ -0,0 +1,40 @@ +import { graphql } from '$houdini'; + +export const ChairPaperDetailSubscription = graphql(` + subscription ChairPaperDetailSubscription($paperId: ID!) { + findFirstResolutionPaper(where: { id: $paperId }) { + id + title + status + content + documentNumber + sequenceNumber + updatedAt + agendaItem { + id + title + } + creator { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairVoteResultSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairVoteResultSubscription.ts new file mode 100644 index 00000000..44add25b --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/[paperId]/chairVoteResultSubscription.ts @@ -0,0 +1,13 @@ +import { graphql } from '$houdini'; + +export const ChairVoteResultSubscription = graphql(` + subscription ChairVoteResultSubscription($paperId: ID!) { + findManyResolutionVoteResult(where: { paperId: $paperId }, limit: 1) { + id + outcome + votesFor + votesAgainst + votesAbstain + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/chairResolutionPapersSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/chairResolutionPapersSubscription.ts new file mode 100644 index 00000000..2b9714f6 --- /dev/null +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/resolutions/chairResolutionPapersSubscription.ts @@ -0,0 +1,40 @@ +import { graphql } from '$houdini'; + +export const ChairResolutionPapersSubscription = graphql(` + subscription ChairResolutionPapersSubscription($committeeId: ID!) { + findManyResolutionPaper(where: { committeeId: $committeeId }) { + id + title + status + documentNumber + sequenceNumber + updatedAt + creatorCommitteeMemberId + agendaItem { + id + title + } + creator { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte index 1e9fae41..807238a2 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte +++ b/src/routes/app/[conferenceId]/[committeeId]/(chairs)/setup/+page.svelte @@ -15,6 +15,7 @@ import StateOfDebate from '$lib/components/committee/StateOfDebateChanger.svelte'; import AgendaItemChanger from '$lib/components/committee/AgendaItemChanger.svelte'; import PresentationSettings from './PresentationSettings.svelte'; + import Tabs from '$lib/components/Tabs.svelte'; import { CommitteeSubscription } from '../committeeSubscription'; import { ScrollArea } from 'bits-ui'; import StatusWidget from '../StatusWidget.svelte'; @@ -42,6 +43,26 @@ } } `); + + const UpdateSelfAddMutation = graphql(` + mutation UpdateSelfAdd( + $committeeId: ID! + $allowDelegationsToAddThemselvesToSpeakersList: Boolean! + ) { + updateCommittee( + id: $committeeId + allowDelegationsToAddThemselvesToSpeakersList: $allowDelegationsToAddThemselvesToSpeakersList + ) { + id + allowDelegationsToAddThemselvesToSpeakersList + } + } + `); + + const selfAddTabs = [ + { id: true, label: m.on(), faIcon: 'fa-check' }, + { id: false, label: m.off(), faIcon: 'fa-xmark' } + ]; {#if committee} @@ -100,6 +121,19 @@ + +

{m.allowSelfAddToSpeakersListDescription()}

+ { + UpdateSelfAddMutation.mutate({ + committeeId: committee.id, + allowDelegationsToAddThemselvesToSpeakersList: tab + }); + }} + /> +

{m.baseFontSizeDescription()}

+
+
+
+ + + localDB.committeeSettings.update(committeeId, { + presentationResolutionFontSize: +(e.target as HTMLInputElement).value + })} + class="range range-primary w-full" + /> + {$committeeSettings?.presentationResolutionFontSize || '?'} +
+
+

{m.resolutionFontSizeDescription()}

diff --git a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/+page.svelte b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/+page.svelte index 5c605a37..3aad924c 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/+page.svelte +++ b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/+page.svelte @@ -24,6 +24,7 @@ import RollCallVotingPresentation from '$lib/components/voting/RollCallVotingPresentation.svelte'; import { browser } from '$app/environment'; import AdoptionConfetti from '$lib/components/AdoptionConfetti.svelte'; + import PresentationResolutionPreview from './PresentationResolutionPreview.svelte'; let { data }: { data: PageData } = $props(); @@ -163,6 +164,16 @@ /> {/if} + + {#if layout.resolutionPreview} + {@const gridProps = layout.resolutionPreview} + + + + {/if} + import { m } from '$lib/paraglide/messages'; + import { + ResolutionPreview, + migrateResolution, + type Resolution, + type ResolutionHeaderData, + type OperativeClause + } from '@deutschemodelunitednations/munify-resolution-editor'; + import Flag from '$lib/components/Flag.svelte'; + import { getTranslatedCountryNameFromAlpha3Code } from '$lib/utils/nationTranslationHelper.svelte'; + import { getResolutionLabels } from '$lib/utils/resolutionEditorLabels'; + import { SvelteMap } from 'svelte/reactivity'; + + interface Props { + resolutionFontSize?: number; + committee: { + abbreviation: string; + name: string; + resolutionHeadline?: string | null; + currentOperativeIndex?: number | null; + currentOperativeClauseId?: string | null; + activeAmendment?: { + id: string; + type: string; + status: string; + documentNumber?: string | null; + targetClauseId?: string | null; + targetOperativeIndex?: number | null; + targetPosition?: number | null; + newContent?: unknown; + proposer?: { + id: string; + representation?: { + name?: string | null; + alpha2Code?: string | null; + alpha3Code?: string | null; + } | null; + } | null; + } | null; + activeDraftResolution?: { + id: string; + content?: unknown; + documentNumber?: string | null; + status: string; + title?: string | null; + updatedAt?: Date | string | null; + agendaItem?: { + id: string; + title?: string | null; + } | null; + creator?: { + id: string; + representation?: { + name?: string | null; + alpha2Code?: string | null; + alpha3Code?: string | null; + } | null; + } | null; + sponsors?: Array<{ + id: string; + committeeMember?: { + representation?: { + name?: string | null; + alpha3Code?: string | null; + } | null; + } | null; + }>; + amendments?: Array<{ + id: string; + type: string; + status: string; + documentNumber?: string | null; + targetClauseId?: string | null; + targetOperativeIndex?: number | null; + targetPosition?: number | null; + newContent?: unknown; + proposer?: { + id: string; + representation?: { + name?: string | null; + } | null; + } | null; + }>; + operativeClauseVotes?: Array<{ + id: string; + clauseId: string; + outcome: string; + }>; + voteResult?: { + outcome: string; + votesFor: number; + votesAgainst: number; + votesAbstain: number; + } | null; + } | null; + conference?: { + title?: string | null; + } | null; + }; + } + + let { committee, resolutionFontSize = 16 }: Props = $props(); + + let dr = $derived(committee.activeDraftResolution); + let activeAmendment = $derived(committee.activeAmendment); + let currentOpIndex = $derived.by(() => { + const clauseId = committee.currentOperativeClauseId; + if (clauseId && resolution) { + const idx = resolution.operative.findIndex((c) => c.id === clauseId); + if (idx !== -1) return idx; + } + return committee.currentOperativeIndex ?? 0; + }); + + let resolution = $derived.by(() => { + if (!dr?.content) return null; + try { + return migrateResolution(dr.content as Resolution) as Resolution; + } catch { + return null; + } + }); + + let currentClause = $derived.by(() => { + if (!resolution) return null; + return resolution.operative[currentOpIndex] ?? null; + }); + + let headerData = $derived.by((): ResolutionHeaderData | undefined => { + if (!dr) return undefined; + return { + conferenceName: committee.conference?.title ?? undefined, + conferenceTitle: committee.conference?.title ?? undefined, + committeeAbbreviation: committee.abbreviation, + committeeFullName: committee.name, + committeeResolutionHeadline: committee.resolutionHeadline ?? undefined, + documentNumber: dr.documentNumber ?? undefined, + topic: dr.agendaItem?.title ?? undefined, + authoringDelegation: + dr.creator?.representation?.name ?? + (dr.creator?.representation?.alpha3Code + ? getTranslatedCountryNameFromAlpha3Code(dr.creator.representation.alpha3Code) + : undefined), + sponsoringDelegations: dr.sponsors + ?.map( + (s) => + getTranslatedCountryNameFromAlpha3Code(s.committeeMember?.representation?.alpha3Code) ?? + s.committeeMember?.representation?.name ?? + '' + ) + .filter(Boolean) + .sort((a, b) => a.localeCompare(b)), + lastEdited: dr.updatedAt ?? undefined + }; + }); + + function getAmendmentTypeLabel(type: string): string { + switch (type) { + case 'DELETE': + return m.deleteClausePresentation(); + case 'ALTER_TEXT': + return m.alterClausePresentation(); + case 'ADD': + return m.addClausePresentation(); + case 'ALTER_POSITION': + return m.moveClausePresentation(); + default: + return type; + } + } + + function getAmendmentTypeBadge(type: string): string { + switch (type) { + case 'DELETE': + return 'badge-error'; + case 'ALTER_TEXT': + return 'badge-warning'; + case 'ADD': + return 'badge-success'; + case 'ALTER_POSITION': + return 'badge-info'; + default: + return 'badge-ghost'; + } + } + + function singleClauseResolution(clause: OperativeClause): Resolution { + return { committeeName: committee.name, preamble: [], operative: [clause] }; + } + + let pendingAmendmentCounts = $derived.by(() => { + if (!dr?.amendments || !resolution) return new SvelteMap(); + const counts = new SvelteMap(); + for (const a of dr.amendments) { + if (a.status !== 'SUBMITTED') continue; + if (a.type !== 'ALTER_TEXT' && a.type !== 'DELETE') continue; + let idx: number | null = null; + if (a.targetClauseId) { + const found = resolution.operative.findIndex((c) => c.id === a.targetClauseId); + if (found !== -1) idx = found; + } else if (a.targetOperativeIndex != null) { + idx = a.targetOperativeIndex; + } + if (idx == null) continue; + counts.set(idx, (counts.get(idx) ?? 0) + 1); + } + return counts; + }); + + // Resolve active amendment's target index from stable clause ID + let resolvedActiveAmendIdx = $derived.by(() => { + if (!activeAmendment || !resolution) return -1; + if (activeAmendment.targetClauseId) { + const idx = resolution.operative.findIndex((c) => c.id === activeAmendment.targetClauseId); + if (idx !== -1) return idx; + } + return activeAmendment.targetOperativeIndex ?? -1; + }); + + function getProposerName( + proposer: + | { + representation?: { + name?: string | null; + alpha2Code?: string | null; + alpha3Code?: string | null; + } | null; + } + | null + | undefined + ): string { + if (!proposer?.representation) return ''; + return ( + proposer.representation.name ?? + (proposer.representation.alpha3Code + ? getTranslatedCountryNameFromAlpha3Code(proposer.representation.alpha3Code) + : '') ?? + '' + ); + } + + +
+ {#if !dr} + +
+ +

{m.noActiveDraftResolution()}

+

{m.setActiveDrHint()}

+
+ {:else if dr.status === 'DRAFT_RESOLUTION' && resolution} + +
+ +
+ {:else if (dr.status === 'AMENDMENT_PHASE' || dr.status === 'VOTING_PHASE') && resolution} + {#if activeAmendment && dr.status === 'AMENDMENT_PHASE'} + +
+
+ + {activeAmendment.documentNumber ?? getAmendmentTypeLabel(activeAmendment.type)} + + {m.proposedAmendmentPresentation()} + {#if activeAmendment.proposer?.representation} +
+ + {getProposerName(activeAmendment.proposer)} +
+ {/if} +
+ + {#if activeAmendment.type === 'DELETE' && resolvedActiveAmendIdx >= 0} + + {@const targetClause = resolution.operative[resolvedActiveAmendIdx]} +
+ {m.operativeClausePresentation()} + {resolvedActiveAmendIdx + 1} +
+ {#if targetClause} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {/if} + {:else if activeAmendment.type === 'ALTER_TEXT' && resolvedActiveAmendIdx >= 0} + + {@const targetClause = resolution.operative[resolvedActiveAmendIdx]} +
+ {m.operativeClausePresentation()} + {resolvedActiveAmendIdx + 1} +
+
+
+
{m.currentText()}
+ {#if targetClause} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {/if} +
+
+
{m.proposedText()}
+ {#if activeAmendment.newContent} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {/if} +
+
+ {:else if activeAmendment.type === 'ADD'} + +
+ {m.insertAfterPresentation({ index: (activeAmendment.targetPosition ?? 0) + 1 })} +
+
+ {#if activeAmendment.newContent} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {/if} +
+ {:else if activeAmendment.type === 'ALTER_POSITION' && resolvedActiveAmendIdx >= 0} + + {@const targetClause = resolution.operative[resolvedActiveAmendIdx]} +
+ {#if targetClause} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {/if} +
+ + + {m.moveToPositionPresentation({ + position: (activeAmendment.targetPosition ?? 0) + 1 + })} + +
+
+ {/if} +
+ {:else} + +
+
+
+ {dr.documentNumber ?? m.draftResolution()} +
+
+ {dr.status === 'VOTING_PHASE' ? m.votingPhase() : m.amendmentPhase()} +
+
+ +
+ + {m.operativeClausePresentation()} + {currentOpIndex + 1} / {resolution.operative.length} + + {#if dr.status === 'AMENDMENT_PHASE' && pendingAmendmentCounts.get(currentOpIndex)} + + {pendingAmendmentCounts.get(currentOpIndex)} + {m.amendmentPhase()} + + {/if} +
+ + {#if currentClause} +
+ + {#snippet previewHeader()}{/snippet} + +
+ {:else} +
+

{m.noOperativeClauses()}

+
+ {/if} +
+ {/if} + {:else} + +
+ +

{m.noActiveDraftResolution()}

+
+ {/if} +
+ + diff --git a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/RegionalGroups.svelte b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/RegionalGroups.svelte index 072f3ee0..1789539c 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/RegionalGroups.svelte +++ b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/RegionalGroups.svelte @@ -36,15 +36,19 @@ committeeMembers.filter((member) => member.representation?.regionalGroup === group); $effect(() => { + let timeout: ReturnType | undefined; if (activeGroup) { - setTimeout( + timeout = setTimeout( () => { activeGroup = nextGroup(activeGroup); }, Math.max(getGroupMembers(activeGroup).length * 500, 5000) ); - console.log(Math.max(getGroupMembers(activeGroup).length * 500, 5000)); } + + return () => { + if (timeout) clearTimeout(timeout); + }; }); diff --git a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/committeeSubscription.ts b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/committeeSubscription.ts index 25d0967b..5544689f 100644 --- a/src/routes/app/[conferenceId]/[committeeId]/(presentation)/committeeSubscription.ts +++ b/src/routes/app/[conferenceId]/[committeeId]/(presentation)/committeeSubscription.ts @@ -6,6 +6,7 @@ export const PresentationSubscription = graphql(` id abbreviation name + resolutionHeadline status statusHeadline statusUntil @@ -56,6 +57,84 @@ export const PresentationSubscription = graphql(` } } } + activeDraftResolutionId + currentOperativeIndex + currentOperativeClauseId + activeAmendmentId + activeAmendment { + id + type + status + documentNumber + targetClauseId + targetOperativeIndex + targetPosition + newContent + proposer { + id + representation { + name + alpha2Code + alpha3Code + } + } + } + activeDraftResolution { + id + content + documentNumber + status + title + updatedAt + agendaItem { + id + title + } + creator { + id + representation { + name + alpha2Code + alpha3Code + } + } + sponsors { + id + committeeMember { + representation { + name + alpha3Code + } + } + } + amendments { + id + type + status + documentNumber + targetClauseId + targetOperativeIndex + targetPosition + newContent + proposer { + id + representation { + name + } + } + } + operativeClauseVotes { + id + clauseId + outcome + } + voteResult { + outcome + votesFor + votesAgainst + votesAbstain + } + } whiteboardContent members { id @@ -71,6 +150,8 @@ export const PresentationSubscription = graphql(` } } conference { + title + resolutionFeatureEnabled uniqueConferenceMembers { id representation { diff --git a/src/routes/app/[conferenceId]/mission-control/DownloadPresenceData.svelte b/src/routes/app/[conferenceId]/mission-control/DownloadPresenceData.svelte index c0539d6e..176b1e9e 100644 --- a/src/routes/app/[conferenceId]/mission-control/DownloadPresenceData.svelte +++ b/src/routes/app/[conferenceId]/mission-control/DownloadPresenceData.svelte @@ -15,7 +15,7 @@ query PresenceDataQuery($conferenceId: ID!) { findManyCommitteeMember(where: { representation: { conferenceId: $conferenceId } }) { id - user { + users { id userEmail } diff --git a/src/routes/app/[conferenceId]/mission-control/config/+page.svelte b/src/routes/app/[conferenceId]/mission-control/config/+page.svelte index b2921afa..e47f8927 100644 --- a/src/routes/app/[conferenceId]/mission-control/config/+page.svelte +++ b/src/routes/app/[conferenceId]/mission-control/config/+page.svelte @@ -2,11 +2,11 @@ import type { PageData } from './$houdini'; import { m } from '$lib/paraglide/messages'; import NavbarBurgerMenu from '$lib/components/NavbarBurgerMenu.svelte'; - import BasicCard from '$lib/components/BasicCard.svelte'; - import { cache, graphql } from '$houdini'; - import { invalidateAll } from '$app/navigation'; - import toast from 'svelte-french-toast'; - import { promiseToastStrings } from '$lib/utils/toast'; + import GeneralTab from './GeneralTab.svelte'; + import UsersTab from './UsersTab.svelte'; + import CommitteesTab from './CommitteesTab.svelte'; + import DelegationsTab from './DelegationsTab.svelte'; + import NsaTab from './NsaTab.svelte'; let { data }: { data: PageData } = $props(); @@ -14,129 +14,17 @@ let conference = $derived(query ? $query.data?.findFirstConference : undefined); let currentUserRole = $derived($query.data?.currentUserRole?.[0]); let isAdmin = $derived(currentUserRole?.conferenceUserType === 'ADMIN'); - let conferenceUsers = $derived( - [...(conference?.users ?? [])].sort((a, b) => a.userEmail.localeCompare(b.userEmail)) - ); let currentUserEmail = $derived(data.user?.email); + let activeTab = $state<'general' | 'users' | 'committees' | 'delegations' | 'nsa'>('general'); + const menubarItems = [ { faIcon: 'fa-rocket-launch', title: m.missionControl(), - href: '..' + href: '.' } ]; - - // Form state - let bulkEmails = $state(''); - let newRole = $state<'ADMIN' | 'TEAM' | 'SPECTATOR' | 'DELEGATE' | 'NON_STATE_ACTOR'>('TEAM'); - let isBulkSubmitting = $state(false); - - const CreateConferenceUserMutation = graphql(` - mutation CreateConferenceUser( - $conferenceId: ID! - $userEmail: String! - $conferenceUserType: ConferenceUserTypeEnum! - ) { - createConferenceUser( - conferenceId: $conferenceId - userEmail: $userEmail - conferenceUserType: $conferenceUserType - ) { - id - userEmail - conferenceUserType - } - } - `); - - const DeleteConferenceUserMutation = graphql(` - mutation DeleteConferenceUser($id: ID!) { - deleteConferenceUser(id: $id) - } - `); - - const UpdateConferenceUserMutation = graphql(` - mutation UpdateConferenceUser($id: ID!, $conferenceUserType: ConferenceUserTypeEnum!) { - updateConferenceUser(id: $id, conferenceUserType: $conferenceUserType) { - id - userEmail - conferenceUserType - } - } - `); - - function isCurrentUser(email: string): boolean { - return currentUserEmail === email; - } - - function isValidEmail(email: string): boolean { - // Simple but robust email validation regex - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - } - - function parseEmails(input: string): string[] { - // Split by newlines, commas, or semicolons, then trim, normalize, and validate - return input - .split(/[\n,;]+/) - .map((email) => email.trim().toLowerCase()) - .filter((email) => email.length > 0 && isValidEmail(email)); - } - - async function addBulkMembers() { - if (!conference?.id || !query) return; - - const emails = parseEmails(bulkEmails); - if (emails.length === 0) return; - - isBulkSubmitting = true; - try { - for (const email of emails) { - await toast.promise( - CreateConferenceUserMutation.mutate({ - conferenceId: conference.id, - userEmail: email, - conferenceUserType: newRole - }), - promiseToastStrings(m.member(), 'add') - ); - } - bulkEmails = ''; - } finally { - isBulkSubmitting = false; - cache.markStale(); - await invalidateAll(); - } - } - - async function removeMember(id: string) { - if (!confirm(m.confirmRemoveMember()) || !query) return; - - await toast.promise( - DeleteConferenceUserMutation.mutate({ id }), - promiseToastStrings(m.member(), 'delete') - ); - cache.markStale(); - await invalidateAll(); - } - - async function updateMemberRole( - id: string, - newType: 'ADMIN' | 'TEAM' | 'SPECTATOR' | 'DELEGATE' | 'NON_STATE_ACTOR' - ) { - if (!query) return; - - await toast.promise( - UpdateConferenceUserMutation.mutate({ - id, - conferenceUserType: newType - }), - promiseToastStrings(m.member(), 'update') - ); - cache.markStale(); - await invalidateAll(); - } @@ -160,106 +48,64 @@ {:else if conference}

{conference.title}

- - -
- - - - - - - - - - {#if conferenceUsers.length === 0} - - - - {:else} - {#each conferenceUsers as user (user.id)} - {@const isSelf = isCurrentUser(user.userEmail)} - - - - - - {/each} - {/if} - -
{m.email()}{m.role()}
- {m.noMembers()} -
- {user.userEmail} - {#if isSelf} - {m.you()} - {/if} - - - - -
-
+
+ + + + + +
- -
- {m.addMember()} -
- -
-
- - -
- -
-
-
-
+ {#if activeTab === 'general'} + + {:else if activeTab === 'users'} + + {:else if activeTab === 'committees'} + + {:else if activeTab === 'delegations'} + + {:else if activeTab === 'nsa'} + + {/if} {:else}
diff --git a/src/routes/app/[conferenceId]/mission-control/config/+page.ts b/src/routes/app/[conferenceId]/mission-control/config/+page.ts index 40963e3d..eadafe89 100644 --- a/src/routes/app/[conferenceId]/mission-control/config/+page.ts +++ b/src/routes/app/[conferenceId]/mission-control/config/+page.ts @@ -7,10 +7,76 @@ export const _houdini_load = graphql(` findFirstConference(where: { id: $conferenceId }) { id title + pressWebsite + hasModeratedCaucus + resolutionFeatureEnabled users { id userEmail conferenceUserType + user { + givenName + familyName + } + committeeMember { + id + representation { + id + name + alpha2Code + alpha3Code + faIcon + } + committee { + id + name + abbreviation + } + } + conferenceMember { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + } + committees { + id + name + abbreviation + members { + id + representation { + id + name + alpha2Code + alpha3Code + faIcon + type + } + } + } + members { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + representations { + id + name + alpha2Code + alpha3Code + type + faIcon } } currentUserRole: findManyConferenceUser( diff --git a/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte new file mode 100644 index 00000000..ee6c5718 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/CommitteesTab.svelte @@ -0,0 +1,233 @@ + + + +
+ + + + + + + + + + + {#if committees.length === 0} + + + + {:else} + {#each committees as committee (committee.id)} + + {#if editingId === committee.id} + + + + + {:else} + + + + + {/if} + + {/each} + {/if} + +
{m.committeeAbbreviation()}{m.committeeName()}{m.committeeMembers()}
{m.noData()}
+ + + + {committee.members.length} +
+ + +
+
{committee.abbreviation}{committee.name}{committee.members.length} +
+ + +
+
+
+ + +
+ {m.addCommittee()} +
+
+ {m.committeeAbbreviation()} + +
+
+ {m.committeeName()} + +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte new file mode 100644 index 00000000..c19c0a78 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/DelegationsTab.svelte @@ -0,0 +1,188 @@ + + + +
+ + + + + + + + + + + + {#if delegations.length === 0} + + + + {:else} + {#each delegations as delegation (delegation.id)} + + + + + + + + {/each} + {/if} + +
{m.name()}Alpha-3{m.committees()}
{m.noData()}
+ + + {delegation.name || getTranslatedCountryNameFromAlpha3Code(delegation.alpha3Code)} + + {delegation.alpha3Code?.toUpperCase() ?? '—'} + + {getCommitteesForDelegation(delegation.id) || '—'} + +
+ + +
+
+
+ +
+ +
+
+ + + + diff --git a/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte b/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte new file mode 100644 index 00000000..361a1060 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/EditConferenceUserModal.svelte @@ -0,0 +1,180 @@ + + + + {#if user} +

{m.editUser()}

+

{user.userEmail}

+ +
+ + +
+ + {#if selectedRole === 'DELEGATE'} +
+ + +
+ {:else if selectedRole === 'NON_STATE_ACTOR'} +
+ + +
+ {:else} +

{m.noAssignmentNeeded()}

+ {/if} + + + {/if} +
diff --git a/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte b/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte new file mode 100644 index 00000000..5625a60b --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/EditDelegationModal.svelte @@ -0,0 +1,174 @@ + + + + {#if delegation} +

{m.edit()}

+ +
+ + + {delegation.name || getTranslatedCountryNameFromAlpha3Code(delegation.alpha3Code)} + +
+ +
+ + {#if committees.length === 0} +

{m.noData()}

+ {:else} +
+ {#each committees as committee (committee.id)} + + {/each} +
+ {/if} +
+ + + {/if} +
diff --git a/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte new file mode 100644 index 00000000..23a2fe24 --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/GeneralTab.svelte @@ -0,0 +1,145 @@ + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte new file mode 100644 index 00000000..312dd5ad --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/NsaTab.svelte @@ -0,0 +1,186 @@ + + + +
+ + + + + + + + + + + {#if nsaActors.length === 0} + + + + {:else} + {#each nsaActors as actor (actor.id)} + + + + + + + {/each} + {/if} + +
{m.icon()}{m.name()}{m.role()}
{m.noData()}
+ + {actor.name ?? '—'} + + {typeLabel[actor.type]?.() ?? actor.type} + + + +
+
+ + +
+ + {m.addNonStateActor()} + +
+
+ {m.name()} + +
+
+ {m.role()} + +
+
+ {m.icon()} + +
+ +
+
+
diff --git a/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte b/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte new file mode 100644 index 00000000..f2cf42fb --- /dev/null +++ b/src/routes/app/[conferenceId]/mission-control/config/UsersTab.svelte @@ -0,0 +1,619 @@ + + + + +
+ +
+ + +
+ + + {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} + + {#each headerGroup.headers as header (header.id)} + + {/each} + + {/each} + + + {#if table.getRowModel().rows.length === 0} + + + + {:else} + {#each table.getRowModel().rows as row (row.id)} + {@const user = row.original} + {@const isSelf = isCurrentUser(user.userEmail)} + + + + + + + + + + + + + + + {/each} + {/if} + +
+ {#if !header.isPlaceholder} +
+ {#if header.id === 'userEmail'} + {m.email()} + {:else if header.id === 'name'} + {m.name()} + {:else if header.id === 'conferenceUserType'} + {m.role()} + {:else if header.id === 'committee'} + {m.committee()} + {:else if header.id === 'assignment'} + {m.assignment()} + {/if} + {#if header.column.getIsSorted() === 'asc'} + + {:else if header.column.getIsSorted() === 'desc'} + + {:else if header.column.getCanSort()} + + {/if} +
+ {/if} +
+ {m.noMembers()} +
+ {user.userEmail} + {#if isSelf} + {m.you()} + {/if} + + {getUserDisplayName(user) || '—'} + + + {roleLabel[user.conferenceUserType]?.() ?? user.conferenceUserType} + + + {#if user.committeeMember?.committee} + {user.committeeMember.committee.abbreviation} + {:else} + + {/if} + + {#if getAssignmentRepresentation(user)} +
+ + {getAssignmentText(user)} +
+ {:else if isAssignableRole(user.conferenceUserType)} + {m.unassigned()} + {:else} + + {/if} +
+
+ + +
+
+
+ + + {#if table.getPageCount() > 1} +
+ + {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + + 1}–{Math.min( + (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length + )} / {table.getFilteredRowModel().rows.length} + +
+ + {#each getVisiblePages(table.getState().pagination.pageIndex, table.getPageCount()) as item, i (item === 'ellipsis' ? `ellipsis-${i}` : item)} + {#if item === 'ellipsis'} + + {:else} + + {/if} + {/each} + +
+
+ {/if} + + +
+ {m.addMember()} +
+ +
+
+ + +
+ +
+
+
+
+ + diff --git a/src/routes/app/[conferenceId]/participant/+layout.svelte b/src/routes/app/[conferenceId]/participant/+layout.svelte new file mode 100644 index 00000000..ae9c9d03 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/src/routes/app/[conferenceId]/participant/+layout.ts b/src/routes/app/[conferenceId]/participant/+layout.ts new file mode 100644 index 00000000..0b4f846c --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+layout.ts @@ -0,0 +1,47 @@ +import { graphql } from '$houdini'; +import type { ParticipantIdentityQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantIdentityQuery($conferenceId: ID!, $userId: ID!) { + findManyConferenceUser(where: { conference: { id: $conferenceId }, user: { id: $userId } }) { + id + conferenceUserType + committeeMemberId + conferenceMemberId + committeeMember { + id + present + committeeId + representation { + id + name + alpha2Code + alpha3Code + type + faIcon + } + } + conferenceMember { + id + representation { + id + name + alpha3Code + type + faIcon + } + } + } + } +`); + +export const _ParticipantIdentityQueryVariables: ParticipantIdentityQueryVariables = async ( + event +) => { + const { user } = await event.parent(); + + return { + conferenceId: event.params.conferenceId, + userId: user.sub + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/+page.svelte b/src/routes/app/[conferenceId]/participant/+page.svelte new file mode 100644 index 00000000..10335f5d --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+page.svelte @@ -0,0 +1,69 @@ + + + + {m.participantView()} - MUNify CHASE + + +{#if role === 'DELEGATE' && !myCommitteeId} + +
+ +

{m.waitingForAssignment()}

+

+ {m.waitingForAssignmentDescription()} +

+ + + {m.back()} + +
+{:else if conference} + + + +
+ +
+ + +{/if} diff --git a/src/routes/app/[conferenceId]/participant/+page.ts b/src/routes/app/[conferenceId]/participant/+page.ts new file mode 100644 index 00000000..62fe7ea9 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/+page.ts @@ -0,0 +1,32 @@ +import { graphql } from '$houdini'; +import type { ParticipantConferenceQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantConferenceQuery($conferenceId: ID!) { + findFirstConference(where: { id: $conferenceId }) { + id + title + committees { + id + name + abbreviation + lastResolutionAdoptionDate + activeAgendaItem { + id + title + } + status + statusHeadline + statusUntil + } + } + } +`); + +export const _ParticipantConferenceQueryVariables: ParticipantConferenceQueryVariables = ( + event +) => { + return { + conferenceId: event.params.conferenceId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte b/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte new file mode 100644 index 00000000..dfe8f635 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/ParticipantIdentityCard.svelte @@ -0,0 +1,31 @@ + + +
+
+ {#if representation} + + {:else} + + {/if} + {displayName} +
+
diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.svelte b/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.svelte new file mode 100644 index 00000000..290c8a81 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.svelte @@ -0,0 +1,64 @@ + + +{#if committee} + + + + +
+ {@render children()} +
+ + +
+ + + {m.committee()} + + {#if committee.conference?.resolutionFeatureEnabled !== false} + + + {m.papers()} + + {/if} +
+{/if} diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.ts new file mode 100644 index 00000000..ddf19dc5 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+layout.ts @@ -0,0 +1,31 @@ +import { graphql } from '$houdini'; +import type { ParticipantCommitteeLayoutQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantCommitteeLayoutQuery($committeeId: ID!) { + findFirstCommittee(where: { id: $committeeId }) { + id + abbreviation + name + resolutionHeadline + supportReEvaluationOpen + activeDraftResolutionId + conference { + title + resolutionFeatureEnabled + } + activeAgendaItem { + id + title + } + } + } +`); + +export const _ParticipantCommitteeLayoutQueryVariables: ParticipantCommitteeLayoutQueryVariables = ( + event +) => { + return { + committeeId: event.params.committeeId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte new file mode 100644 index 00000000..27babb45 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.svelte @@ -0,0 +1,199 @@ + + + + {committee?.name ?? m.committee()} - MUNify CHASE + + +{#if committee} + +
+ +
+ +
+ + +
+
+ + + {#if committee.statusHeadline} +
{committee.statusHeadline}
+ {/if} +
+
+
+
+

{m.majorities()}

+ +
+
+ + + {#if isParticipant || role === 'SPECTATOR'} + {#each [{ list: speakersList, label: m.speakersList(), myPosition: myPositionOnSpeakers }, { list: commentList, label: m.commentList(), myPosition: myPositionOnComments }] as { list, label, myPosition }} + {#if list} +
+
+

{label}

+ + + + + + + {#if canSelfAdd} + {#if myPosition !== null} + +
+ {#if myPosition === 0} + + {m.youreUp()} + + {:else} + + {m.onListPosition({ position: String(myPosition) })} + + {/if} + +
+ {:else if list.isClosed} +
+ + {m.listClosedCannotAdd()} +
+ {:else if role === 'DELEGATE' && !myPresent} +
+ + {m.notPresentCannotAdd()} +
+ {:else} + + {/if} + {/if} +
+
+ {/if} + {/each} + {/if} + + + {#if committee.showWhiteboard && committee.whiteboardContent} +
+
+

{m.whiteboard()}

+ +
+
+ {/if} +
+{/if} diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts new file mode 100644 index 00000000..c7d6d16e --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/+page.ts @@ -0,0 +1,81 @@ +import { graphql } from '$houdini'; +import type { ParticipantCommitteeQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantCommitteeQuery($committeeId: ID!) { + findFirstCommittee(where: { id: $committeeId }) { + id + abbreviation + name + status + statusHeadline + statusUntil + showWhiteboard + whiteboardContent + allowDelegationsToAddThemselvesToSpeakersList + totalPresent + simpleMajority + twoThirdsMajority + paperSupportThreshold + activeAgendaItem { + id + title + speakersList { + id + type + isClosed + speakingTime + startTimestamp + timeLeft + speakers { + id + position + overwriteName + committeeMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + present + } + conferenceMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + } + } + } + } + members { + id + present + representation { + id + type + name + alpha2Code + faIcon + } + } + } + } +`); + +export const _ParticipantCommitteeQueryVariables: ParticipantCommitteeQueryVariables = (event) => { + return { + committeeId: event.params.committeeId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts new file mode 100644 index 00000000..ef928ddc --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/committeeSubscription.ts @@ -0,0 +1,85 @@ +import { graphql } from '$houdini'; + +export const ParticipantCommitteeSubscription = graphql(` + subscription ParticipantCommitteeSubscription($id: ID!) { + findFirstCommittee(where: { id: $id }) { + id + abbreviation + name + resolutionHeadline + status + statusHeadline + statusUntil + showWhiteboard + whiteboardContent + allowDelegationsToAddThemselvesToSpeakersList + supportReEvaluationOpen + amendmentSubmissionOpen + amendmentSponsoringOpen + activeDraftResolutionId + totalPresent + simpleMajority + twoThirdsMajority + paperSupportThreshold + currentOperativeIndex + currentOperativeClauseId + activeAmendmentId + activeAgendaItem { + id + title + speakersList { + id + type + isClosed + speakingTime + startTimestamp + timeLeft + speakers { + id + position + overwriteName + committeeMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + present + } + conferenceMember { + id + representation { + id + type + name + regionalGroup + alpha2Code + alpha3Code + faIcon + } + } + } + } + } + conference { + title + } + members { + id + present + representation { + id + type + name + alpha2Code + faIcon + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.svelte b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.svelte new file mode 100644 index 00000000..7c17d7a1 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.svelte @@ -0,0 +1,369 @@ + + + + {m.papers()} - MUNify CHASE + + +
+ +
+

{m.myPapers()}

+ + +
+ {#if isDelegate} + {#if activeAgendaItem} + + {:else} +
+ +
+ {/if} + {/if} + +
+ e.key === 'Enter' && handleRedeemCode()} + /> + +
+
+ + + {#if myPapers.length === 0} +
+ {m.noPapersYet()} +
+ {:else} + + {/if} +
+ + +
+
+

{m.draftResolutions()}

+ {#if committee?.supportReEvaluationOpen} + {m.supportReEvaluation()} + {/if} +
+ + {#if draftResolutions.length === 0} +
+ {m.noDraftResolutionsYet()} +
+ {:else} +
+ {#each draftResolutions as paper} + {@const isSupportingDr = paper.sponsors.some( + (s) => s.committeeMemberId === myCommitteeMemberId + )} + {@const isActiveDr = paper.id === committee?.activeDraftResolutionId} + + {/each} +
+ {/if} +
+
diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.ts new file mode 100644 index 00000000..4fbe3a21 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/+page.ts @@ -0,0 +1,43 @@ +import { graphql } from '$houdini'; +import type { ParticipantPapersQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantPapersQuery($committeeId: ID!) { + findManyResolutionPaper(where: { committeeId: $committeeId }) { + id + title + status + updatedAt + creatorCommitteeMemberId + documentNumber + creator { + id + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + sponsors { + committeeMemberId + committeeMember { + representation { + name + alpha2Code + faIcon + } + } + } + editors { + conferenceUserId + } + } + } +`); + +export const _ParticipantPapersQueryVariables: ParticipantPapersQueryVariables = (event) => { + return { + committeeId: event.params.committeeId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.svelte b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.svelte new file mode 100644 index 00000000..f048a799 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.svelte @@ -0,0 +1,1566 @@ + + + + {paper?.documentNumber ?? paper?.title ?? m.untitledPaper()} - MUNify CHASE + + +{#if paper} +
+ +
+ + + {m.back()} + +
+ {#if saveStatus === 'saving'} + + {m.savingChanges()} + + {:else if saveStatus === 'saved'} + + {m.changesSaved()} + + {:else if saveStatus === 'error'} + + {m.saveError()} + + {/if} + {#if canDelete} + + {/if} + + + +
+
+ + +
+ +
+
+ {#if paper.documentNumber} + {paper.documentNumber} + {:else} + {paper.title || m.untitledPaper()} + {/if} + + {paper.status === 'WORKING_PAPER' + ? m.workingPaper() + : paper.status === 'SUBMITTED' + ? m.submitted() + : paper.status === 'DRAFT_RESOLUTION' + ? m.draftResolution() + : paper.status === 'AMENDMENT_PHASE' + ? m.amendmentPhase() + : paper.status === 'VOTING_PHASE' + ? m.votingPhase() + : m.finalResolution()} + +
+
+
+ + {#if canEdit} +
+ +
+ {/if} + + +
+
+ {#each sortedSponsors as sponsor} +
+ +
+ {/each} +
+

+ {m.supporterCount({ count: String(paper.sponsors.length) })} +

+ {#if canSponsor && myCommitteeMemberId && !isDrStatus} + + + {:else if canToggleDrSupport && myCommitteeMemberId} + +
+ {m.supportReEvaluation()} + +
+ {/if} +
+ + + {#if canManageShareCodes} +
+ {#if paper.shareCodes.length > 0} +
+ {#each paper.shareCodes as shareCode} +
+ {shareCode.code} + + {shareCode.permission === 'EDIT' ? m.editAccess() : m.sponsor()} + + + +
+ {/each} +
+ {/if} +
+ + +
+
+ {/if} +
+
+ + + {#if canEdit && collaborativeMode && hasOtherLocks} +
+ + {m.collaborativeEditingInfo()} +
+ {/if} + + + {#if paper.status === 'FINAL' && voteResult} +
+ +
+ + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + + + {m.votesFor()}: {voteResult.votesFor} | {m.votesAgainst()}: {voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0} + | {m.votesAbstain()}: {voteResult.votesAbstain} + {/if} + +
+
+ {/if} + + + {#if paper.status === 'VOTING_PHASE'} +
+ + {m.votingPhaseActive()} +
+ {/if} + + +
+ {#if resolution} + {@const collab = canEdit && collaborativeMode} + + {#snippet betweenOperativeClauses({ index })} + {#if showAmendmentUI && isDelegate && committeeSubscriptionData?.amendmentSubmissionOpen} +
+ +
+ {/if} + {/snippet} + {#snippet preambleAnnotations({ clause })} + {@const lock = locksByClauseId.get(clause.id)} + {#if lock} +
+
+ {#if lock.conferenceUser?.committeeMember?.representation} + + {/if} + +
+
+ {/if} + {/snippet} + {#snippet clauseAnnotations({ clause })} + {@const lock = locksByClauseId.get(clause.id)} + {#if lock} +
+
+ {#if lock.conferenceUser?.committeeMember?.representation} + + {/if} + +
+
+ {/if} + {/snippet} + {#snippet preambleClauseToolbar({ clause })} + {#if showComments} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/if} + {/snippet} + {#snippet clauseToolbar({ clause })} + {#if showComments} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/if} + {/snippet} + {#snippet afterPreambleClause({ clause })} + {#if showComments && !canEdit} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/if} + {/snippet} + {#snippet afterOperativeClause({ clause, index })} + {#if showComments && !canEdit} + + onCreateComment(content, visibility, parentCommentId, clause.id)} + {onUpdateComment} + {onDeleteComment} + /> + {/if} + {/snippet} +
+ {/if} +
+ + + {#if showAmendmentUI} + +
+ + {m.currentParagraph()}: OP {currentOpIndex + 1} +
+ + + {#if myAmendments.length > 0 || mySponsoredAmendments.length > 0} +
+
+ {#each myAmendments as amendment (amendment.id)} + {@const sponsorCount = amendment.sponsors?.length ?? 0} + {@const isActive = amendment.id === activeAmendmentId} +
+
+
+ + {amendment.documentNumber ?? getAmendmentTypeLabel(amendment.type)} + + + {getAmendmentStatusLabel(amendment.status)} + + {#if amendment.targetClauseId || amendment.targetOperativeIndex != null} + {@const resolvedOpIdx = amendment.targetClauseId + ? operativeClauses.findIndex((c) => c.id === amendment.targetClauseId) + : (amendment.targetOperativeIndex ?? -1)} + {#if resolvedOpIdx >= 0} + + OP {resolvedOpIdx + 1} + + {/if} + {/if} + {#if isActive} + {m.activeAmendment()} + {/if} +
+ + +
+ + + {m.sponsorThreshold({ + current: String(sponsorCount), + needed: String(sponsorThresholdNeeded), + percent: '10' + })} + +
+
+
+ {/each} + + {#each mySponsoredAmendments as amendment (amendment.id)} + {@const isActive = amendment.id === activeAmendmentId} +
+
+
+ + {amendment.documentNumber ?? getAmendmentTypeLabel(amendment.type)} + + {#if amendment.targetClauseId || amendment.targetOperativeIndex != null} + {@const resolvedOpIdx = amendment.targetClauseId + ? operativeClauses.findIndex((c) => c.id === amendment.targetClauseId) + : (amendment.targetOperativeIndex ?? -1)} + {#if resolvedOpIdx >= 0} + + OP {resolvedOpIdx + 1} + + {/if} + {/if} + {#if isActive} + {m.activeAmendment()} + {/if} + {#if amendment.proposer?.representation} +
+ + + {m.proposedBy({ + name: + amendment.proposer.representation.name ?? + getTranslatedCountryNameFromAlpha3Code( + amendment.proposer.representation.alpha3Code + ) ?? + '' + })} + +
+ {/if} +
+
+
+ {/each} +
+
+ {/if} + + + {@const otherPendingAmendments = allAmendments.filter( + (a) => + a.status === 'SUBMITTED' && + a.proposerCommitteeMemberId !== myCommitteeMemberId && + !a.sponsors?.some((s) => s.committeeMemberId === myCommitteeMemberId) + )} + {#if otherPendingAmendments.length > 0 && isDelegate && committeeSubscriptionData?.amendmentSponsoringOpen} +
+
+ {#each otherPendingAmendments as amendment (amendment.id)} +
+
+
+ + {amendment.documentNumber ?? getAmendmentTypeLabel(amendment.type)} + + {#if amendment.targetClauseId || amendment.targetOperativeIndex != null} + {@const resolvedOpIdx = amendment.targetClauseId + ? operativeClauses.findIndex((c) => c.id === amendment.targetClauseId) + : (amendment.targetOperativeIndex ?? -1)} + {#if resolvedOpIdx >= 0} + + OP {resolvedOpIdx + 1} + + {/if} + {/if} + {#if amendment.proposer?.representation} +
+ +
+ {/if} + + {amendment.sponsors?.length ?? 0}/{sponsorThresholdNeeded} + +
+ +
+
+ {/each} +
+
+ {/if} + {/if} + + + {#if paper.status === 'FINAL' && clauseVotes.length > 0} +
+
+ {#each operativeClauses as clause, i (clause.id)} + {@const vote = clauseVoteMap.get(clause.id)} +
+ OP {i + 1} + {#if vote} + + {vote.outcome === 'ADOPTED' ? m.adopted() : m.rejected()} + + + {vote.votesFor}/{vote.votesAgainst} + {#if vote.votesAbstain > 0}/{vote.votesAbstain}{/if} + + {:else} + + {/if} +
+ {/each} + {#if voteResult} +
+
+ {m.finalVote()} + + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + + + {voteResult.votesFor}/{voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0}/{voteResult.votesAbstain}{/if} + +
+ {/if} +
+
+ {/if} + + + {#if showComments && (commentsByClauseId.get(null)?.length ?? 0) > 0} +
+ + onCreateComment(content, visibility, parentCommentId, null)} + {onUpdateComment} + {onDeleteComment} + /> +
+ {/if} + + + {#if canSubmit} +
+ +
+ {/if} +
+ + + +
+

{m.submitToChair()}

+

{m.confirmSubmitPaper()}

+
+ + +
+
+
+ + + +
+

{m.deletePaper()}

+

{m.confirmDeletePaper()}

+
+ + +
+
+
+ + + +{/if} diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.ts new file mode 100644 index 00000000..1dd58399 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/+page.ts @@ -0,0 +1,54 @@ +import { graphql } from '$houdini'; +import type { ParticipantPaperDetailQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query ParticipantPaperDetailQuery($paperId: ID!) { + findFirstResolutionPaper(where: { id: $paperId }) { + id + title + status + content + documentNumber + creatorCommitteeMemberId + updatedAt + creator { + id + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + } + shareCodes { + id + code + permission + } + editors { + id + conferenceUserId + } + } + } +`); + +export const _ParticipantPaperDetailQueryVariables: ParticipantPaperDetailQueryVariables = ( + event +) => { + return { + paperId: event.params.paperId + }; +}; diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/amendmentsSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/amendmentsSubscription.ts new file mode 100644 index 00000000..1c476fd7 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/amendmentsSubscription.ts @@ -0,0 +1,40 @@ +import { graphql } from '$houdini'; + +export const ParticipantAmendmentsSubscription = graphql(` + subscription ParticipantAmendmentsSubscription($paperId: ID!) { + findManyAmendment(where: { paperId: $paperId }) { + id + type + status + documentNumber + targetClauseId + targetOperativeIndex + targetPosition + newContent + proposerCommitteeMemberId + createdAt + proposer { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + id + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/commentsSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/commentsSubscription.ts new file mode 100644 index 00000000..084c0df9 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/commentsSubscription.ts @@ -0,0 +1,31 @@ +import { graphql } from '$houdini'; + +export const ParticipantPaperCommentsSubscription = graphql(` + subscription ParticipantPaperCommentsSubscription($paperId: ID!) { + findManyResolutionComment(where: { paperId: $paperId }) { + id + clauseId + content + visibility + parentCommentId + createdAt + updatedAt + author { + id + user { + givenName + familyName + } + committeeMember { + representation { + name + alpha2Code + alpha3Code + faIcon + } + } + conferenceUserType + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/lockSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/lockSubscription.ts new file mode 100644 index 00000000..67c761da --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/lockSubscription.ts @@ -0,0 +1,22 @@ +import { graphql } from '$houdini'; + +export const PaperClauseLocksSubscription = graphql(` + subscription PaperClauseLocksSubscription($paperId: ID!) { + findManyPaperClauseLock(where: { paperId: $paperId }) { + id + clauseId + conferenceUserId + acquiredAt + conferenceUser { + committeeMember { + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/paperDetailSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/paperDetailSubscription.ts new file mode 100644 index 00000000..45f358fa --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/[paperId]/paperDetailSubscription.ts @@ -0,0 +1,45 @@ +import { graphql } from '$houdini'; + +export const ParticipantPaperDetailSubscription = graphql(` + subscription ParticipantPaperDetailSubscription($paperId: ID!) { + findFirstResolutionPaper(where: { id: $paperId }) { + id + title + status + content + documentNumber + creatorCommitteeMemberId + updatedAt + creator { + id + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + sponsors { + id + committeeMemberId + committeeMember { + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + } + shareCodes { + id + code + permission + } + editors { + id + conferenceUserId + } + } + } +`); diff --git a/src/routes/app/[conferenceId]/participant/[committeeId]/papers/papersSubscription.ts b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/papersSubscription.ts new file mode 100644 index 00000000..7a4c00e1 --- /dev/null +++ b/src/routes/app/[conferenceId]/participant/[committeeId]/papers/papersSubscription.ts @@ -0,0 +1,36 @@ +import { graphql } from '$houdini'; + +export const ParticipantPapersSubscription = graphql(` + subscription ParticipantPapersSubscription($committeeId: ID!) { + findManyResolutionPaper(where: { committeeId: $committeeId }) { + id + title + status + updatedAt + creatorCommitteeMemberId + documentNumber + creator { + id + representation { + name + alpha3Code + alpha2Code + faIcon + } + } + sponsors { + committeeMemberId + committeeMember { + representation { + name + alpha2Code + faIcon + } + } + } + editors { + conferenceUserId + } + } + } +`); diff --git a/src/routes/app/print/[documentId]/+page.svelte b/src/routes/app/print/[documentId]/+page.svelte new file mode 100644 index 00000000..7f40da48 --- /dev/null +++ b/src/routes/app/print/[documentId]/+page.svelte @@ -0,0 +1,135 @@ + + +
+ {#if resolution} + + + {#if showVotingResults} +
+

{m.votingResults()}

+ + {#if clauseVotes.length > 0} + + + + + + + + + + + + {#each operativeClauses as clause, i (clause.id)} + {@const vote = clauseVoteMap.get(clause.id)} + {#if vote} + + + + + + + + {/if} + {/each} + {#if voteResult} + + + + + + + + {/if} + +
 {m.outcome()}{m.votesFor()}{m.votesAgainst()}{m.votesAbstain()}
OP {i + 1}{vote.outcome === 'ADOPTED' ? m.adopted() : m.rejected()}{vote.votesFor}{vote.votesAgainst}{vote.votesAbstain}
{m.finalVote()} + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + {voteResult.votesFor}{voteResult.votesAgainst}{voteResult.votesAbstain}
+ {:else if voteResult} +
+ {m.finalVote()}: + {voteResult.outcome === 'ADOPTED' + ? m.adopted() + : voteResult.outcome === 'REJECTED' + ? m.rejected() + : m.sentBack()} + — {m.votesFor()}: {voteResult.votesFor} | {m.votesAgainst()}: {voteResult.votesAgainst} + {#if voteResult.votesAbstain > 0} + | {m.votesAbstain()}: {voteResult.votesAbstain} + {/if} +
+ {/if} +
+ {/if} + {:else} +
+ +
+ {/if} +
diff --git a/src/routes/app/print/[documentId]/+page.ts b/src/routes/app/print/[documentId]/+page.ts new file mode 100644 index 00000000..b518d567 --- /dev/null +++ b/src/routes/app/print/[documentId]/+page.ts @@ -0,0 +1,61 @@ +import { graphql } from '$houdini'; +import type { PrintPaperQueryVariables } from './$houdini'; + +export const _houdini_load = graphql(` + query PrintPaperQuery($documentId: ID!) { + findFirstResolutionPaper(where: { id: $documentId }) { + id + content + status + documentNumber + updatedAt + agendaItem { + title + } + creator { + representation { + name + alpha3Code + } + } + sponsors { + id + committeeMember { + representation { + name + alpha3Code + } + } + } + committee { + abbreviation + name + resolutionHeadline + conference { + title + } + } + } + findManyOperativeClauseVote(where: { paperId: $documentId }) { + id + clauseId + outcome + votesFor + votesAgainst + votesAbstain + } + findManyResolutionVoteResult(where: { paperId: $documentId }, limit: 1) { + id + outcome + votesFor + votesAgainst + votesAbstain + } + } +`); + +export const _PrintPaperQueryVariables: PrintPaperQueryVariables = (event) => { + return { + documentId: event.params.documentId + }; +}; diff --git a/vite.config.ts b/vite.config.ts index 0a931684..55de23af 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,17 +2,64 @@ import houdini from 'houdini/vite'; import { paraglideVitePlugin } from '@inlang/paraglide-js'; import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { defineConfig, type ViteDevServer } from 'vite'; + +function devAutoRestart() { + const RACE_CONDITION_PATTERNS = [ + 'has not been implemented', // Pothos ObjectRef race condition + 'Class extends value undefined is not a constructor or null' // Houdini store race condition + ]; + + return { + name: 'dev-auto-restart', + configureServer(server: ViteDevServer) { + let restarting = false; + + const triggerRestart = (label: string) => { + if (restarting) return; + restarting = true; + console.warn(`\n⚠️ ${label}, restarting dev server...\n`); + server.restart(); + }; + + const isRaceCondition = (message: string | undefined) => + RACE_CONDITION_PATTERNS.some((pattern) => message?.includes(pattern)); + + const onUnhandledRejection = (reason: unknown) => { + if (reason instanceof Error && isRaceCondition(reason.message)) { + triggerRestart('Race condition detected'); + } + }; + + process.on('unhandledRejection', onUnhandledRejection); + server.httpServer?.on('close', () => { + process.off('unhandledRejection', onUnhandledRejection); + }); + + const originalSsrFixStacktrace = server.ssrFixStacktrace; + server.ssrFixStacktrace = function (e: Error) { + originalSsrFixStacktrace.call(this, e); + if (isRaceCondition(e?.message)) { + triggerRestart('SSR race condition detected'); + } + }; + } + }; +} export default defineConfig({ plugins: [ + devAutoRestart(), tailwindcss(), paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide', - strategy: ['url', 'baseLocale'] + strategy: ['cookie', 'preferredLanguage', 'baseLocale'] }), houdini(), sveltekit() - ] + ], + server: { + allowedHosts: ['svelte-dev.munify.cloud'] + } });