diff --git a/.gitignore b/.gitignore index 5ef6a52..401454f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + + +# environment +.env +.env*.local diff --git a/package-lock.json b/package-lock.json index 6c9c8c7..05e1fc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,48 @@ { - "name": "kita-planer-tmp", + "name": "kita-planer", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "kita-planer-tmp", + "name": "kita-planer", "version": "0.1.0", "dependencies": { + "@auth/prisma-adapter": "^2.11.2", + "@prisma/client": "^6.19.3", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@react-email/components": "^1.0.12", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^1.14.0", "next": "16.2.4", + "next-auth": "^5.0.0-beta.31", + "next-themes": "^0.4.6", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "resend": "^6.12.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^4.4.2" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", + "prisma": "^6.19.3", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } }, @@ -36,6 +59,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.11.2.tgz", + "integrity": "sha512-GyNEUNtrPgDPs0M4xX6F5i7jTsCKwU6BXV9zutctcoo6K1Ud+juckrmQS11uyNgeWsw6sliextHbU/e+8lsizQ==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -310,6 +374,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1241,6 +1747,985 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-email/body": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.3.0.tgz", + "integrity": "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/button": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.1.tgz", + "integrity": "sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-block": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.1.tgz", + "integrity": "sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/code-inline": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.6.tgz", + "integrity": "sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/column": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.14.tgz", + "integrity": "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/components": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.12.tgz", + "integrity": "sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "@react-email/body": "0.3.0", + "@react-email/button": "0.2.1", + "@react-email/code-block": "0.2.1", + "@react-email/code-inline": "0.0.6", + "@react-email/column": "0.0.14", + "@react-email/container": "0.0.16", + "@react-email/font": "0.0.10", + "@react-email/head": "0.0.13", + "@react-email/heading": "0.0.16", + "@react-email/hr": "0.0.12", + "@react-email/html": "0.0.12", + "@react-email/img": "0.0.12", + "@react-email/link": "0.0.13", + "@react-email/markdown": "0.0.18", + "@react-email/preview": "0.0.14", + "@react-email/render": "2.0.6", + "@react-email/row": "0.0.13", + "@react-email/section": "0.0.17", + "@react-email/tailwind": "2.0.7", + "@react-email/text": "0.1.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/components/node_modules/@react-email/render": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.6.tgz", + "integrity": "sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/container": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz", + "integrity": "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/font": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.10.tgz", + "integrity": "sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/head": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.13.tgz", + "integrity": "sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/heading": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.16.tgz", + "integrity": "sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/hr": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.12.tgz", + "integrity": "sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/html": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.12.tgz", + "integrity": "sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/img": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.12.tgz", + "integrity": "sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/link": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.13.tgz", + "integrity": "sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/markdown": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.18.tgz", + "integrity": "sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "marked": "^15.0.12" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/preview": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.14.tgz", + "integrity": "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/row": { + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz", + "integrity": "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/section": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.17.tgz", + "integrity": "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/@react-email/tailwind": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.7.tgz", + "integrity": "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "tailwindcss": "^4.1.18" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-email/body": ">=0", + "@react-email/button": ">=0", + "@react-email/code-block": ">=0", + "@react-email/code-inline": ">=0", + "@react-email/container": ">=0", + "@react-email/heading": ">=0", + "@react-email/hr": ">=0", + "@react-email/img": ">=0", + "@react-email/link": ">=0", + "@react-email/preview": ">=0", + "@react-email/text": ">=0", + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@react-email/body": { + "optional": true + }, + "@react-email/button": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } + } + }, + "node_modules/@react-email/text": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz", + "integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1248,6 +2733,32 @@ "dev": true, "license": "MIT" }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1539,6 +3050,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1574,7 +3092,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -1585,8 +3103,9 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2220,6 +3739,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2462,6 +3993,15 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/brace-expansion": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", @@ -2521,6 +4061,35 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2618,12 +4187,59 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2651,6 +4267,23 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2677,7 +4310,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2741,6 +4374,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2766,6 +4409,25 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -2802,6 +4464,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2812,6 +4488,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2825,6 +4507,74 @@ "node": ">=0.10.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2840,6 +4590,17 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.349", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz", @@ -2854,6 +4615,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", @@ -2868,6 +4639,18 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3045,6 +4828,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3260,6 +5085,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3475,6 +5301,36 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3526,6 +5382,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -3616,6 +5478,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3702,6 +5579,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3747,6 +5633,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3921,6 +5825,41 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4424,12 +6363,21 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4543,6 +6491,15 @@ "node": ">=0.10" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4864,6 +6821,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", + "integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -4874,6 +6840,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5032,6 +7010,43 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -5079,6 +7094,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -5086,6 +7108,40 @@ "dev": true, "license": "MIT" }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5209,6 +7265,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5290,6 +7353,19 @@ "node": ">=6" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5317,6 +7393,29 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5336,6 +7435,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5346,6 +7457,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", @@ -5375,6 +7492,26 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5385,6 +7522,57 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5407,6 +7595,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5428,6 +7633,17 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -5458,6 +7674,89 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5502,6 +7801,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resend": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.3.tgz", + "integrity": "sha512-FkEi6YPnVL96/LvH8+QP7NaeaBy5brYXwlRqUCqZZeNL0/iyKij18IPmyPXYauT/2ODn1JG04qKz+qlJfzqzTw==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "svix": "1.92.2" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -5642,6 +7962,18 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5858,6 +8190,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5874,6 +8216,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -6073,12 +8425,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svix": { + "version": "1.92.2", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.92.2.tgz", + "integrity": "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } }, "node_modules/tapable": { "version": "2.3.3", @@ -6094,6 +8474,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -6201,6 +8591,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6296,7 +8706,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -6433,6 +8843,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6572,7 +9025,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", - "dev": true, "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 13b090c..dba973d 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,57 @@ { - "name": "kita-planer-tmp", + "name": "kita-planer", "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "NODE_ENV=development prisma db push && NODE_ENV=development prisma db seed && next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:studio": "prisma studio", + "db:seed": "NODE_ENV=development prisma db seed", + "db:seed:reset": "NODE_ENV=development prisma db seed -- --reset" + }, + "prisma": { + "seed": "tsx prisma/seed.ts" }, "dependencies": { + "@auth/prisma-adapter": "^2.11.2", + "@prisma/client": "^6.19.3", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@react-email/components": "^1.0.12", + "bcryptjs": "^3.0.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^1.14.0", "next": "16.2.4", + "next-auth": "^5.0.0-beta.31", + "next-themes": "^0.4.6", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "resend": "^6.12.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^4.4.2" }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.2.4", + "prisma": "^6.19.3", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" } } diff --git a/planung.md b/planung.md new file mode 100644 index 0000000..aef72c1 --- /dev/null +++ b/planung.md @@ -0,0 +1,127 @@ +Master-Spezifikation: SaaS Kita-Planer für Elternvereine +1. Projektübersicht +Entwicklung einer mandantenfähigen (Multi-Tenant) Web-Anwendung zur Organisation von Elternvereinen/Elterninitiativen (Kitas). Das Kernziel ist die Digitalisierung des Notdienstes, der Terminplanung und der Stammdatenverwaltung. Das System wird als SaaS-Lösung (Software as a Service) aufgebaut, sodass sich unabhängige Kitas selbstständig registrieren und die Plattform nutzen können. + +2. Architektur & Tech-Stack +Um externe Kosten (Vendor-Lock-in) zu vermeiden, wird ein komplett offener und flexibel hostbarer Stack verwendet: + +Framework: Next.js (App Router, React) + +Datenbank: PostgreSQL (Lokal oder Cloud-hosted) + +ORM: Prisma ORM + +Authentifizierung: NextAuth.js (Auth.js) mit E-Mail/Passwort-Credentials + +Styling & UI: Tailwind CSS, shadcn/ui, Lucide Icons + +E-Mail-Versand: Nodemailer oder Resend (für Notdienst-Alarme und Einladungen) + +Mandantenfähigkeit (Multi-Tenancy): Umsetzung strikt auf Datenbank- und Applikationsebene. Jede Kita erhält bei Registrierung eine eindeutige kita_id (Tenant-ID). Strenge Regel: Jeder Datensatz in der Datenbank (User, Kinder, Termine, Notdienste etc.) muss diese kita_id referenzieren. + +3. Rollenkonzept (RBAC) +Super-Admin: Systembetreuer (verwaltet die Infrastruktur, operiert außerhalb des normalen Kita-Alltags). + +Admin (Vorstand): Kann alles innerhalb seiner Kita verwalten (Einstellungen ändern, Eltern einladen, Termine freigeben, Ämter verteilen). Der Gründer einer Kita erhält diese Rolle automatisch. + +Notdienst-Koordinator: Spezielle Berechtigung für bestimmte Elternteile. Darf den Notdienst-Plan generieren, manuell anpassen und den Notdienst-Alarm auslösen. + +Elternteil: Standard-Nutzer. Kann eigene Verfügbarkeiten eintragen, Termine sehen, das interne Adressbuch nutzen und Mitbringsel bei Festen eintragen. + +4. Feature-Module +Modul 0: SaaS-Onboarding & Einrichtung +Landingpage: Öffentliche Startseite zur kostenlosen Registrierung einer neuen Kita. + +Registrierung: Gründer gibt E-Mail/Passwort ein und wird automatisch erster Admin eines neuen Mandanten (kita_id). + +Einrichtungs-Wizard: + +Name der Kita festlegen. + +Module aktivieren/deaktivieren (z.B. Notdienst-Modul). + +Spezifische Regeln definieren (z.B. "Mindestanzahl Notdienst-Termine pro Kind im Monat"). + +Modul 1: Notdienst-Planung (Kern-Feature) +Dateneingabe: Eltern müssen für den Folgemonat X Termine pro Kind als Verfügbarkeit eintragen. Das System validiert die Eingabe anhand der Kita-Einstellungen. + +Erinnerung (Cronjob): Automatischer E-Mail-Versand an säumige Eltern ca. eine Woche vor Ende der Eintragsfrist. + +Plan-Generierung: Der Notdienst-Koordinator löst die Verteilung aus. Der Algorithmus verteilt die Tage zufällig, aber fair über alle eingegebenen Verfügbarkeiten (Start jeden Monat bei null). + +Manuelle Bearbeitung: Der generierte Plan ist zunächst ein Entwurf. Der Koordinator kann Tage überschreiben oder Lücken manuell füllen (z.B. nach Absprache in Messenger-Gruppen), bevor der Plan "veröffentlicht" wird. + +Alarmierung (Workflow): + +Bei Krankmeldung einer Fachkraft wählt der Koordinator den aktuellen Tag und klickt auf "Notdienst auslösen". + +Das eingeteilte Elternteil erhält sofort eine E-Mail mit einem Bestätigungslink. + +In der App sieht der Koordinator den Status: "Wartend" (gelbes Lade-Icon). + +Klickt das Elternteil den Link, wechselt der Status auf "Bestätigt" (grün). + +Fallback: Erfolgt keine Bestätigung, klärt der Koordinator den Rest außerhalb der App. + +Modul 2: Terminkalender & Essenslisten +Kalender-Sichtbarkeit: Alle bestätigten Termine sind für alle eingeloggten Nutzer einer Kita sichtbar. Kategorien: Kita-Fest, Schließtag, Interner Geburtstag, Externer Geburtstag, Sonstiges. + +Buchungs-Workflow: + +Eltern können Tage für private Nutzung (z.B. Kindergeburtstage) "anfragen". Status ist Ausstehend und der Slot wird blockiert. + +Admin (Vorstand) muss die Anfrage bestätigen oder ablehnen. + +Admins können Termine (z.B. Anfragen von Externen) direkt anlegen. + +Flexible Mitbring-Listen: Der Admin kann zu jedem Termin eine Essens-/Mitbringliste aktivieren. Eltern können über ein freies Textfeld Einträge hinzufügen (z.B. "Familie Müller: Nudelsalat") und ihre eigenen Einträge bearbeiten/löschen. + +Modul 3: Eltern-, Kinder- und Ämterverwaltung +Onboarding (Invite-Only): Admins legen neue Eltern (Vorname, Nachname, E-Mail) im System an und verknüpft sie mit den entsprechenden Kindern. Das System sendet einen Einladungs-Link zur Passwortvergabe. + +Feste Elterndienste (Ämter): Jedem Eltern-Account kann durch den Admin ein fester Dienst zugewiesen werden (z.B. "Wäschedienst", "Einkauf"). Dies ist auf dem Profil für alle sichtbar. + +Kita-Adressbuch (Opt-In): Nach expliziter Zustimmung sind Eltern mit ihren Kontaktdaten im internen Kita-Adressbuch für andere Eltern sichtbar (zur Erleichterung von Spielverabredungen). + +Modul 4: ErzieherInnen-Verwaltung +Werden als reine Stammdaten (Namen) vom Admin in einer separaten Tabelle gepflegt. + +Sie dienen ausschließlich als Referenz für die Notdienst-Logik (Auswahl: "Wer ist heute krank?"). + +Sie erhalten keine System-Accounts/Logins. + +Modul 5: Sicherheit, DSGVO & Privacy by Design +Dieses System verarbeitet sensible, personenbezogene Daten von Kindern. Das Architektur- und Datenbankdesign muss kompromisslos sicher sein. Folgende Regeln sind beim Schreiben des Codes strikt einzuhalten: + +1. Strikte Mandanten-Isolierung (Tenant Isolation): + +Jede Datenbankabfrage (Find, Update, Delete) muss zwingend den Filter where: { kita_id: user.kita_id } enthalten. + +Es darf niemals möglich sein, dass durch Manipulation von API-Routen oder URLs ein Nutzer einer Kita auf den Datensatz einer anderen Kita zugreift. Dies ist durch Middleware oder sichere Server Actions zu validieren. + +2. DSGVO-Einwilligung (Consent Logging): + +Das User-Modell in Prisma muss Felder wie privacy_policy_accepted_at (DateTime) und directory_opt_in_at (DateTime) erhalten. + +Ohne gesetzten Zeitstempel bei der Datenschutzerklärung wird der Zugriff auf die App-Inhalte blockiert (Redirect zum Onboarding-Screen). + +3. Datenminimierung & Löschkonzept ("Recht auf Vergessenwerden"): + +Es wird striktes onDelete: Cascade im Prisma-Schema verwendet. Löscht ein Admin seine Kita, müssen alle zugehörigen User, Kinder, Termine und Notdienste rückstandslos aus der Datenbank entfernt werden. + +Nutzer müssen in ihren Profil-Einstellungen einen Button "Account löschen" haben, der ihre Daten und die ihrer Kinder (sofern kein anderer verknüpfter Elternteil existiert) endgültig vernichtet. Keine "Soft-Deletes" für personenbezogene Daten. + +4. Authentifizierung & Transport-Sicherheit: + +Vollständige Nutzung von NextAuth.js für sicheres Session-Management (HTTP-only, Secure Cookies, CSRF-Schutz). + +Sicheres Hashing von Passwörtern. + +5. API-Schutz (Rate Limiting): + +Kritische Endpunkte (Login, Registrierung, Passwort-Reset) müssen vor Brute-Force-Attacken geschützt werden. + +6. Entwicklungs-Fokus für die KI +Bitte beginne mit der Initialisierung des Next.js Projekts und der Erstellung des schema.prisma. +Achte von der ersten Zeile an penibel auf die Mandantenfähigkeit (kita_id) in allen relevanten Tabellen und auf die korrekten Relationen zwischen Kitas, Usern (Eltern), Kindern und Terminen. +Präsentiere mir zuerst das geplante schema.prisma zur Abnahme, bevor wir mit dem Frontend oder den API-Routen beginnen. \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..d56a1c8 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,512 @@ +// ===================================================================== +// Kita-Planer · Prisma Schema +// --------------------------------------------------------------------- +// Multi-Tenant-SaaS für Elternvereine. Tenant = `Kita`. +// +// Sicherheits-Leitplanken (siehe planung.md §5): +// • Jedes mandantengebundene Modell trägt `kitaId` und ist über +// `Kita` per `onDelete: Cascade` verknüpft → DSGVO-konformes +// "Recht auf Vergessenwerden". +// • Auf Applikations-/Server-Action-Ebene MUSS jeder Query mit +// `where: { kitaId: session.user.kitaId }` versehen werden. +// • Keine Soft-Deletes auf personenbezogene Daten. +// ===================================================================== + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ===================================================================== +// ENUMS +// ===================================================================== + +/// Rollen-basierte Zugriffskontrolle (RBAC). +/// SUPERADMIN existiert systemweit (kitaId optional); +/// ADMIN, KOORDINATOR und ELTERN sind immer einer Kita zugeordnet. +enum UserRole { + SUPERADMIN + ADMIN + KOORDINATOR + ELTERN +} + +enum InvitationStatus { + PENDING + ACCEPTED + EXPIRED + REVOKED +} + +enum TerminType { + KITA_FEST + SCHLIESSTAG + TEAMTAG + MITMACH_TAG + ELTERNABEND + MITGLIEDERVERSAMMLUNG + ELTERNCAFE + GEBURTSTAG_INTERN + GEBURTSTAG_EXTERN + SONSTIGES +} + +enum TerminStatus { + PENDING + CONFIRMED + REJECTED + CANCELLED +} + +enum NotdienstPlanStatus { + DRAFT + PUBLISHED +} + +enum NotdienstAlertStatus { + PENDING + CONFIRMED + CANCELLED +} + +// ===================================================================== +// TENANT +// ===================================================================== + +model Kita { + id String @id @default(cuid()) + name String + slug String @unique + + // Modul-Aktivierung (Einrichtungs-Wizard, Modul 0) + notdienstModuleEnabled Boolean @default(true) + terminModuleEnabled Boolean @default(true) + adressbuchModuleEnabled Boolean @default(true) + + // Mandantenspezifische Regeln + notdienstMinPerChildPerMonth Int @default(2) + notdienstReminderDaysBefore Int @default(7) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations (Cascade auf alle Tenant-Daten) + users User[] + children Child[] + educators Educator[] + parentDuties ParentDuty[] + parentDutyAssignments ParentDutyAssignment[] + invitations Invitation[] + termine Termin[] + mitbringselItems MitbringselItem[] + notdienstPlans NotdienstPlan[] + notdienstAvailabilities NotdienstAvailability[] + notdienstAssignments NotdienstAssignment[] + notdienstAlerts NotdienstAlert[] + childParents ChildParent[] + + @@map("kitas") +} + +// ===================================================================== +// USERS +// ===================================================================== + +model User { + id String @id @default(cuid()) + + /// Mandantenzuordnung. Nullable nur für `SUPERADMIN`. + /// Bei Löschung der Kita werden alle zugeordneten User kaskadiert. + kitaId String? + kita Kita? @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + email String @unique + passwordHash String + + firstName String + lastName String + + role UserRole @default(ELTERN) + + // Adressbuch / Kontaktdaten (nur sichtbar bei directoryOptIn) + phone String? + street String? + postalCode String? + city String? + + // ---- DSGVO Consent Logging (planung.md §5.2) ---- + /// Zeitstempel der Annahme der Datenschutzerklärung. + /// Ohne diesen Wert blockiert die App den Zugriff (Redirect → Onboarding). + privacyPolicyAcceptedAt DateTime? + /// Versionsstring der akzeptierten Datenschutzerklärung (z.B. "2025-04-01"). + privacyPolicyVersion String? + /// Zeitstempel des Opt-Ins für das interne Kita-Adressbuch (Modul 3). + directoryOptInAt DateTime? + + // Auth-Bookkeeping + emailVerifiedAt DateTime? + lastLoginAt DateTime? + failedLoginAttempts Int @default(0) + lockedUntil DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + childLinks ChildParent[] + dutyAssignments ParentDutyAssignment[] + + notdienstAvailabilities NotdienstAvailability[] + notdienstAlertsAssigned NotdienstAlert[] @relation("NotdienstAlertParent") + notdienstAlertsTriggered NotdienstAlert[] @relation("NotdienstAlertTrigger") + notdienstPlansCreated NotdienstPlan[] @relation("NotdienstPlanCreator") + + termineCreated Termin[] @relation("TerminCreator") + termineApproved Termin[] @relation("TerminApprover") + + mitbringselItems MitbringselItem[] + invitationsCreated Invitation[] @relation("InvitationCreator") + + @@index([kitaId]) + @@index([kitaId, role]) + @@map("users") +} + +// ===================================================================== +// CHILDREN +// ===================================================================== + +model Child { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + firstName String + lastName String + dateOfBirth DateTime? + notes String? + + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + parentLinks ChildParent[] + notdienstAvailabilities NotdienstAvailability[] + notdienstAssignments NotdienstAssignment[] + + @@index([kitaId]) + @@map("children") +} + +/// Verknüpfung Kind ↔ Elternteil (m:n). +/// Kaskadiert über beide Seiten, damit das Löschen eines Users +/// oder Kindes keine Datenleichen hinterlässt. +model ChildParent { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + childId String + child Child @relation(fields: [childId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + + @@unique([childId, userId]) + @@index([kitaId]) + @@index([userId]) + @@map("child_parents") +} + +// ===================================================================== +// EDUCATORS (ErzieherInnen — reine Stammdaten, keine Logins, Modul 4) +// ===================================================================== + +model Educator { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + firstName String + lastName String + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + notdienstAlerts NotdienstAlert[] + + @@index([kitaId]) + @@map("educators") +} + +// ===================================================================== +// PARENT DUTIES (Feste Ämter / Elterndienste, Modul 3) +// ===================================================================== + +model ParentDuty { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + name String + description String? + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + assignments ParentDutyAssignment[] + + @@unique([kitaId, name]) + @@index([kitaId]) + @@map("parent_duties") +} + +model ParentDutyAssignment { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + dutyId String + duty ParentDuty @relation(fields: [dutyId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + + @@unique([dutyId, userId]) + @@index([kitaId]) + @@index([userId]) + @@map("parent_duty_assignments") +} + +// ===================================================================== +// INVITATIONS (Invite-Only Onboarding, Modul 3) +// ===================================================================== + +model Invitation { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + email String + role UserRole @default(ELTERN) + + token String @unique + status InvitationStatus @default(PENDING) + expiresAt DateTime + acceptedAt DateTime? + + invitedById String? + invitedBy User? @relation("InvitationCreator", fields: [invitedById], references: [id], onDelete: SetNull) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([kitaId, email]) + @@index([kitaId]) + @@map("invitations") +} + +// ===================================================================== +// TERMINE (Kalender, Modul 2) +// ===================================================================== + +model Termin { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + title String + description String? + type TerminType + status TerminStatus @default(PENDING) + + startDate DateTime + endDate DateTime + allDay Boolean @default(false) + + /// Mitbringliste pro Termin aktivierbar (Modul 2, "Flexible Mitbring-Listen") + mitbringselListEnabled Boolean @default(false) + + createdById String? + createdBy User? @relation("TerminCreator", fields: [createdById], references: [id], onDelete: SetNull) + + approvedById String? + approvedBy User? @relation("TerminApprover", fields: [approvedById], references: [id], onDelete: SetNull) + approvedAt DateTime? + rejectionReason String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + mitbringselItems MitbringselItem[] + + @@index([kitaId, startDate]) + @@index([kitaId, status]) + @@map("termine") +} + +model MitbringselItem { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + terminId String + termin Termin @relation(fields: [terminId], references: [id], onDelete: Cascade) + + /// Cascade: Beim Löschen eines Accounts werden dessen Mitbringsel-Einträge + /// rückstandslos mitgelöscht (DSGVO "Recht auf Vergessenwerden"). + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + content String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([kitaId]) + @@index([terminId]) + @@map("mitbringsel_items") +} + +// ===================================================================== +// NOTDIENST (Modul 1 – Kern-Feature) +// ===================================================================== + +/// Monatlicher Notdienst-Plan. Erst DRAFT (Koordinator bearbeitet), +/// nach "Veröffentlichung" PUBLISHED. +model NotdienstPlan { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + year Int + /// Monat 1–12. + month Int + status NotdienstPlanStatus @default(DRAFT) + + createdById String? + createdBy User? @relation("NotdienstPlanCreator", fields: [createdById], references: [id], onDelete: SetNull) + + publishedAt DateTime? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + assignments NotdienstAssignment[] + + @@unique([kitaId, year, month]) + @@index([kitaId]) + @@map("notdienst_plans") +} + +/// Verfügbarkeits-Eintrag eines Elternteils für ein Kind an einem Tag. +/// Pro (Kind, Datum) maximal ein Eintrag — Geschwister-Doppelbuchungen +/// werden über die App-Logik geblockt. +model NotdienstAvailability { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + childId String + child Child @relation(fields: [childId], references: [id], onDelete: Cascade) + + /// Eintragender User (für Audit / Erinnerungs-Cronjob). + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + date DateTime @db.Date + + createdAt DateTime @default(now()) + + @@unique([childId, date]) + @@index([kitaId, date]) + @@index([userId]) + @@map("notdienst_availabilities") +} + +/// Eingeteilter Notdienst-Slot — Ergebnis der Plan-Generierung +/// bzw. manueller Bearbeitung durch den Koordinator. +model NotdienstAssignment { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + planId String + plan NotdienstPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + + childId String + child Child @relation(fields: [childId], references: [id], onDelete: Cascade) + + date DateTime @db.Date + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + alerts NotdienstAlert[] + + @@unique([planId, date]) + @@index([kitaId, date]) + @@map("notdienst_assignments") +} + +/// Aktive Alarmierung — wird bei Krankmeldung einer Fachkraft +/// vom Koordinator ausgelöst. Bestätigung via Magic-Link (Token). +model NotdienstAlert { + id String @id @default(cuid()) + kitaId String + kita Kita @relation(fields: [kitaId], references: [id], onDelete: Cascade) + + assignmentId String + assignment NotdienstAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) + + /// Eingeteiltes Elternteil (Empfänger des Alarms). + parentUserId String + parentUser User @relation("NotdienstAlertParent", fields: [parentUserId], references: [id], onDelete: Cascade) + + /// Auslösender Koordinator. + triggeredById String? + triggeredBy User? @relation("NotdienstAlertTrigger", fields: [triggeredById], references: [id], onDelete: SetNull) + + /// Optional: kranke Fachkraft (Referenz für Reporting). + educatorId String? + educator Educator? @relation(fields: [educatorId], references: [id], onDelete: SetNull) + + status NotdienstAlertStatus @default(PENDING) + + /// Einmal-Token für den Bestätigungslink in der Alarm-Mail. + confirmationToken String @unique + triggeredAt DateTime @default(now()) + confirmedAt DateTime? + + notes String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([kitaId]) + @@index([assignmentId]) + @@index([parentUserId]) + @@map("notdienst_alerts") +} + +// ===================================================================== +// AUTH-HILFSTABELLE +// ===================================================================== + +/// Tokens für Passwort-Reset und E-Mail-Verifikation. +/// Kompatibel mit dem NextAuth-Schema, falls später Email-Provider aktiviert wird. +model VerificationToken { + identifier String + token String @unique + expires DateTime + + @@unique([identifier, token]) + @@map("verification_tokens") +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..505ffd0 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,675 @@ +import { + InvitationStatus, + NotdienstAlertStatus, + NotdienstPlanStatus, + PrismaClient, + TerminStatus, + TerminType, + UserRole, +} from "@prisma/client"; +import { hash } from "bcryptjs"; + +// ===================================================================== +// Kita-Planer · Dev-Seed +// --------------------------------------------------------------------- +// Default: nicht destruktiv. Seed-Daten werden nur angelegt, wenn die DB +// noch keine Kita und keine User enthaelt. +// +// Reset: `npm run db:seed:reset` loescht gezielt die Demo-Daten und baut +// sie neu auf. Production wird nur mit ALLOW_DATABASE_SEED=true erlaubt. +// ===================================================================== + +const prisma = new PrismaClient(); + +const DEMO_KITA_SLUG = "waldameisen"; +const DEFAULT_PASSWORD = "password123"; +const PRIVACY_POLICY_VERSION = "2026-05-01"; +const RESET_MODE = process.argv.includes("--reset"); + +const DEMO_USER_EMAILS = [ + "super@kita-planer.local", + "admin@waldameisen.local", + "mueller@waldameisen.local", + "schmidt@waldameisen.local", + "yilmaz@waldameisen.local", + "pending@waldameisen.local", +]; + +const DEMO_VERIFICATION_TOKENS = [ + "demo-pending-parent-token", + "demo-invite-token", +]; + +type SeedContext = Awaited>; + +function dateOnly(date: Date) { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); +} + +function addDays(date: Date, days: number) { + const next = new Date(date); + next.setDate(next.getDate() + days); + return dateOnly(next); +} + +function startOfMonth(date: Date) { + return new Date(date.getFullYear(), date.getMonth(), 1); +} + +function getPreviousMonth(date: Date) { + return new Date(date.getFullYear(), date.getMonth() - 1, 1); +} + +function getLastDayOfPreviousMonth(date: Date) { + return dateOnly(new Date(date.getFullYear(), date.getMonth(), 0)); +} + +function getTargetMonth(date: Date) { + return new Date(date.getFullYear(), date.getMonth() + 1, 1); +} + +function getWorkingDays(year: number, month: number, limit?: number) { + const days: Date[] = []; + const cursor = new Date(year, month - 1, 1); + + while (cursor.getMonth() === month - 1) { + const day = cursor.getDay(); + if (day !== 0 && day !== 6) { + days.push(dateOnly(cursor)); + } + cursor.setDate(cursor.getDate() + 1); + } + + return typeof limit === "number" ? days.slice(0, limit) : days; +} + +function isSeedAllowed() { + if ( + process.env.NODE_ENV === "development" || + process.env.ALLOW_DATABASE_SEED === "true" + ) { + return true; + } + + console.log( + "Seed uebersprungen: NODE_ENV ist nicht development. Setze ALLOW_DATABASE_SEED=true, wenn das bewusst gewuenscht ist.", + ); + return false; +} + +async function isDatabaseEmpty() { + const [kitaCount, userCount] = await Promise.all([ + prisma.kita.count(), + prisma.user.count(), + ]); + + return kitaCount === 0 && userCount === 0; +} + +async function resetDemoData() { + const demoUsers = await prisma.user.findMany({ + where: { + OR: [ + { email: { in: DEMO_USER_EMAILS } }, + { kita: { slug: DEMO_KITA_SLUG } }, + ], + }, + select: { id: true }, + }); + + await prisma.verificationToken.deleteMany({ + where: { + OR: [ + { token: { in: DEMO_VERIFICATION_TOKENS } }, + { identifier: { in: demoUsers.map((user) => user.id) } }, + ], + }, + }); + + await prisma.kita.deleteMany({ where: { slug: DEMO_KITA_SLUG } }); + await prisma.user.deleteMany({ where: { email: { in: DEMO_USER_EMAILS } } }); +} + +async function createDemoUsers(passwordHash: string, consentAt: Date) { + const superAdmin = await prisma.user.create({ + data: { + email: "super@kita-planer.local", + passwordHash, + firstName: "Sys", + lastName: "Admin", + role: UserRole.SUPERADMIN, + privacyPolicyAcceptedAt: consentAt, + privacyPolicyVersion: PRIVACY_POLICY_VERSION, + emailVerifiedAt: consentAt, + }, + }); + + const kita = await prisma.kita.create({ + data: { + name: "Waldameisen e.V.", + slug: DEMO_KITA_SLUG, + notdienstModuleEnabled: true, + terminModuleEnabled: true, + adressbuchModuleEnabled: true, + notdienstMinPerChildPerMonth: 2, + notdienstReminderDaysBefore: 7, + }, + }); + + const admin = await prisma.user.create({ + data: { + kitaId: kita.id, + email: "admin@waldameisen.local", + passwordHash, + firstName: "Anna", + lastName: "Vorstand", + role: UserRole.ADMIN, + privacyPolicyAcceptedAt: consentAt, + privacyPolicyVersion: PRIVACY_POLICY_VERSION, + directoryOptInAt: consentAt, + emailVerifiedAt: consentAt, + phone: "+49 30 1000 1000", + street: "Kitaweg 1", + postalCode: "10115", + city: "Berlin", + }, + }); + + const koordinator = await prisma.user.create({ + data: { + kitaId: kita.id, + email: "mueller@waldameisen.local", + passwordHash, + firstName: "Maria", + lastName: "Mueller", + role: UserRole.KOORDINATOR, + privacyPolicyAcceptedAt: consentAt, + privacyPolicyVersion: PRIVACY_POLICY_VERSION, + directoryOptInAt: consentAt, + emailVerifiedAt: consentAt, + phone: "+49 30 1234 5678", + street: "Beispielweg 1", + postalCode: "10115", + city: "Berlin", + }, + }); + + const elternSchmidt = await prisma.user.create({ + data: { + kitaId: kita.id, + email: "schmidt@waldameisen.local", + passwordHash, + firstName: "Lukas", + lastName: "Schmidt", + role: UserRole.ELTERN, + privacyPolicyAcceptedAt: consentAt, + privacyPolicyVersion: PRIVACY_POLICY_VERSION, + emailVerifiedAt: consentAt, + }, + }); + + const elternYilmaz = await prisma.user.create({ + data: { + kitaId: kita.id, + email: "yilmaz@waldameisen.local", + passwordHash, + firstName: "Aylin", + lastName: "Yilmaz", + role: UserRole.ELTERN, + privacyPolicyAcceptedAt: consentAt, + privacyPolicyVersion: PRIVACY_POLICY_VERSION, + directoryOptInAt: consentAt, + emailVerifiedAt: consentAt, + phone: "+49 30 9876 5432", + street: "Parkstrasse 8", + postalCode: "10405", + city: "Berlin", + }, + }); + + const pendingParent = await prisma.user.create({ + data: { + kitaId: kita.id, + email: "pending@waldameisen.local", + passwordHash: "", + firstName: "Lena", + lastName: "Fischer", + role: UserRole.ELTERN, + }, + }); + + return { + kita, + superAdmin, + admin, + koordinator, + elternSchmidt, + elternYilmaz, + pendingParent, + }; +} + +async function createChildren({ + kita, + koordinator, + elternSchmidt, + elternYilmaz, + pendingParent, +}: SeedContext) { + const anna = await prisma.child.create({ + data: { + kitaId: kita.id, + firstName: "Anna", + lastName: "Mueller", + dateOfBirth: new Date("2021-03-15"), + parentLinks: { + create: { kitaId: kita.id, userId: koordinator.id }, + }, + }, + }); + + const ben = await prisma.child.create({ + data: { + kitaId: kita.id, + firstName: "Ben", + lastName: "Mueller", + dateOfBirth: new Date("2023-07-22"), + notes: "Geschwisterkind von Anna.", + parentLinks: { + create: { kitaId: kita.id, userId: koordinator.id }, + }, + }, + }); + + const clara = await prisma.child.create({ + data: { + kitaId: kita.id, + firstName: "Clara", + lastName: "Schmidt", + dateOfBirth: new Date("2022-11-03"), + parentLinks: { + create: { kitaId: kita.id, userId: elternSchmidt.id }, + }, + }, + }); + + const emil = await prisma.child.create({ + data: { + kitaId: kita.id, + firstName: "Emil", + lastName: "Yilmaz", + dateOfBirth: new Date("2021-09-09"), + parentLinks: { + create: [ + { kitaId: kita.id, userId: elternYilmaz.id }, + { kitaId: kita.id, userId: elternSchmidt.id }, + ], + }, + }, + }); + + const nina = await prisma.child.create({ + data: { + kitaId: kita.id, + firstName: "Nina", + lastName: "Fischer", + dateOfBirth: new Date("2022-05-30"), + parentLinks: { + create: { kitaId: kita.id, userId: pendingParent.id }, + }, + }, + }); + + return { anna, ben, clara, emil, nina }; +} + +async function createEducators(kitaId: string) { + const sabine = await prisma.educator.create({ + data: { kitaId, firstName: "Sabine", lastName: "Schulze" }, + }); + const petra = await prisma.educator.create({ + data: { kitaId, firstName: "Petra", lastName: "Klein" }, + }); + const jonas = await prisma.educator.create({ + data: { kitaId, firstName: "Jonas", lastName: "Wagner" }, + }); + const norbert = await prisma.educator.create({ + data: { + kitaId, + firstName: "Norbert", + lastName: "Altmann", + active: false, + }, + }); + + return { sabine, petra, jonas, norbert }; +} + +async function createParentDuties({ + kita, + koordinator, + elternSchmidt, + elternYilmaz, +}: SeedContext) { + const waesche = await prisma.parentDuty.create({ + data: { + kitaId: kita.id, + name: "Waeschedienst", + description: "Kita-Waesche waschen und zurueckbringen.", + }, + }); + + const einkauf = await prisma.parentDuty.create({ + data: { + kitaId: kita.id, + name: "Einkauf", + description: "Woechentlicher Einkauf fuer die Kita-Kueche.", + }, + }); + + const garten = await prisma.parentDuty.create({ + data: { + kitaId: kita.id, + name: "Gartendienst", + description: "Aussenbereich pflegen und Spielmaterial pruefen.", + }, + }); + + await prisma.parentDutyAssignment.createMany({ + data: [ + { kitaId: kita.id, dutyId: waesche.id, userId: elternSchmidt.id }, + { kitaId: kita.id, dutyId: einkauf.id, userId: koordinator.id }, + { kitaId: kita.id, dutyId: garten.id, userId: elternYilmaz.id }, + ], + }); +} + +async function createInvites({ kita, admin, pendingParent }: SeedContext) { + const expires = addDays(new Date(), 7); + + await prisma.invitation.create({ + data: { + kitaId: kita.id, + email: "invite@waldameisen.local", + role: UserRole.ELTERN, + token: "demo-invite-token", + status: InvitationStatus.PENDING, + expiresAt: expires, + invitedById: admin.id, + }, + }); + + await prisma.verificationToken.create({ + data: { + identifier: pendingParent.id, + token: "demo-pending-parent-token", + expires, + }, + }); +} + +async function createTermine({ + kita, + admin, + koordinator, + elternSchmidt, + elternYilmaz, +}: SeedContext) { + const now = new Date(); + + const elternabend = await prisma.termin.create({ + data: { + kitaId: kita.id, + title: "Elternabend Fruehling", + description: "Austausch zu Terminen, Notdienst und Sommerplanung.", + type: TerminType.ELTERNABEND, + status: TerminStatus.CONFIRMED, + startDate: addDays(now, 10), + endDate: addDays(now, 10), + allDay: false, + mitbringselListEnabled: true, + createdById: admin.id, + approvedById: admin.id, + approvedAt: now, + }, + }); + + await prisma.termin.create({ + data: { + kitaId: kita.id, + title: "Teamtag", + description: "Die Kita bleibt wegen interner Fortbildung geschlossen.", + type: TerminType.TEAMTAG, + status: TerminStatus.CONFIRMED, + startDate: addDays(now, 18), + endDate: addDays(now, 18), + allDay: true, + createdById: admin.id, + approvedById: admin.id, + approvedAt: now, + }, + }); + + await prisma.termin.create({ + data: { + kitaId: kita.id, + title: "Elterncafe im Garten", + description: "Anfrage eines Elternteils, noch nicht bestaetigt.", + type: TerminType.ELTERNCAFE, + status: TerminStatus.PENDING, + startDate: addDays(now, 24), + endDate: addDays(now, 24), + allDay: false, + createdById: elternSchmidt.id, + }, + }); + + await prisma.termin.create({ + data: { + kitaId: kita.id, + title: "Spontaner Ausflug", + description: "Beispiel fuer eine abgelehnte Terminanfrage.", + type: TerminType.SONSTIGES, + status: TerminStatus.REJECTED, + startDate: addDays(now, -6), + endDate: addDays(now, -6), + allDay: true, + createdById: elternYilmaz.id, + approvedById: koordinator.id, + approvedAt: addDays(now, -5), + rejectionReason: "Zu kurzfristig fuer die Gruppe.", + }, + }); + + await prisma.mitbringselItem.createMany({ + data: [ + { + kitaId: kita.id, + terminId: elternabend.id, + userId: koordinator.id, + content: "Obstplatte", + }, + { + kitaId: kita.id, + terminId: elternabend.id, + userId: elternSchmidt.id, + content: "Brot und Aufstrich", + }, + ], + }); +} + +async function createNotdienstData( + { kita, koordinator, elternSchmidt, elternYilmaz }: SeedContext, + children: Awaited>, + educators: Awaited>, +) { + const today = dateOnly(new Date()); + const currentMonth = startOfMonth(today); + const previousMonth = getPreviousMonth(today); + const previousAssignmentDate = getLastDayOfPreviousMonth(today); + const targetMonth = getTargetMonth(today); + + const availabilityPairs = [ + { childId: children.anna.id, userId: koordinator.id }, + { childId: children.clara.id, userId: elternSchmidt.id }, + { childId: children.emil.id, userId: elternYilmaz.id }, + { childId: children.ben.id, userId: koordinator.id }, + ]; + + const targetWorkingDays = getWorkingDays( + targetMonth.getFullYear(), + targetMonth.getMonth() + 1, + 12, + ); + + await prisma.notdienstAvailability.createMany({ + data: targetWorkingDays.map((date, index) => { + const pair = availabilityPairs[index % availabilityPairs.length]; + return { + kitaId: kita.id, + childId: pair.childId, + userId: pair.userId, + date, + }; + }), + }); + + const currentPlan = await prisma.notdienstPlan.create({ + data: { + kitaId: kita.id, + year: currentMonth.getFullYear(), + month: currentMonth.getMonth() + 1, + status: NotdienstPlanStatus.PUBLISHED, + createdById: koordinator.id, + publishedAt: new Date(), + }, + }); + + await prisma.notdienstAssignment.create({ + data: { + kitaId: kita.id, + planId: currentPlan.id, + childId: children.anna.id, + date: today, + }, + }); + + const previousPlan = await prisma.notdienstPlan.create({ + data: { + kitaId: kita.id, + year: previousMonth.getFullYear(), + month: previousMonth.getMonth() + 1, + status: NotdienstPlanStatus.PUBLISHED, + createdById: koordinator.id, + publishedAt: addDays(today, -10), + }, + }); + + const pastAssignment = await prisma.notdienstAssignment.create({ + data: { + kitaId: kita.id, + planId: previousPlan.id, + childId: children.clara.id, + date: previousAssignmentDate, + }, + }); + + await prisma.notdienstAlert.create({ + data: { + kitaId: kita.id, + assignmentId: pastAssignment.id, + parentUserId: elternSchmidt.id, + triggeredById: koordinator.id, + educatorId: educators.sabine.id, + status: NotdienstAlertStatus.CONFIRMED, + confirmationToken: "demo-confirmed-alert-token", + triggeredAt: previousAssignmentDate, + confirmedAt: previousAssignmentDate, + notes: "Beispiel-Alarm aus Seed-Daten.", + }, + }); +} + +async function createDemoData() { + console.log("Seeding Kita-Planer demo data..."); + + const passwordHash = await hash(DEFAULT_PASSWORD, 12); + const consentAt = new Date(); + const context = await createDemoUsers(passwordHash, consentAt); + const children = await createChildren(context); + const educators = await createEducators(context.kita.id); + + await createParentDuties(context); + await createInvites(context); + await createTermine(context); + await createNotdienstData(context, children, educators); + + printSummary(context, children); +} + +async function seedIfEmpty() { + if (!(await isDatabaseEmpty())) { + console.log("Seed uebersprungen: Datenbank enthaelt bereits Kita- oder User-Daten."); + return; + } + + await createDemoData(); +} + +function printSummary( + { + kita, + superAdmin, + admin, + koordinator, + elternSchmidt, + elternYilmaz, + pendingParent, + }: SeedContext, + children: Awaited>, +) { + console.log("Seed complete"); + console.log(""); + console.log(` Kita: ${kita.name} (${kita.slug})`); + console.log(""); + console.log(` Logins (Passwort jeweils: ${DEFAULT_PASSWORD})`); + console.log(` Superadmin: ${superAdmin.email}`); + console.log(` Admin: ${admin.email}`); + console.log(` Koordinator: ${koordinator.email}`); + console.log(` Eltern: ${elternSchmidt.email}`); + console.log(` Eltern: ${elternYilmaz.email}`); + console.log(""); + console.log( + ` Offener Invite: ${pendingParent.email} -> /invite/demo-pending-parent-token`, + ); + console.log( + ` Kinder: ${Object.values(children) + .map((child) => `${child.firstName} ${child.lastName}`) + .join(", ")}`, + ); +} + +async function main() { + if (!isSeedAllowed()) { + return; + } + + if (RESET_MODE) { + console.log("Reset demo seed data..."); + await resetDemoData(); + await createDemoData(); + return; + } + + await seedIfEmpty(); +} + +main() + .catch((error) => { + console.error("Seed failed:", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/app/alert/[token]/actions.ts b/src/app/alert/[token]/actions.ts new file mode 100644 index 0000000..745d292 --- /dev/null +++ b/src/app/alert/[token]/actions.ts @@ -0,0 +1,34 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { NotdienstAlertStatus } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; + +export async function confirmAlertAction(token: string) { + try { + const alert = await prisma.notdienstAlert.findUnique({ + where: { confirmationToken: token }, + }); + + if (!alert) { + return { error: "Ungültiger Bestätigungs-Link." }; + } + + if (alert.status !== NotdienstAlertStatus.PENDING) { + return { error: "Dieser Alarm wurde bereits bestätigt oder abgebrochen." }; + } + + await prisma.notdienstAlert.update({ + where: { id: alert.id }, + data: { + status: NotdienstAlertStatus.CONFIRMED, + confirmedAt: new Date(), + }, + }); + + revalidatePath("/dashboard"); + return { success: true }; + } catch (error: any) { + return { error: "Fehler bei der Bestätigung." }; + } +} diff --git a/src/app/alert/[token]/alert-confirm-form.tsx b/src/app/alert/[token]/alert-confirm-form.tsx new file mode 100644 index 0000000..6310c47 --- /dev/null +++ b/src/app/alert/[token]/alert-confirm-form.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useTransition } from "react"; +import { Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { confirmAlertAction } from "./actions"; + +export function AlertConfirmForm({ token }: { token: string }) { + const [isPending, startTransition] = useTransition(); + + const handleConfirm = () => { + startTransition(async () => { + const result = await confirmAlertAction(token); + if ("error" in result && result.error) { + toast.error(result.error as string); + } else { + toast.success("Erfolgreich bestätigt!"); + } + }); + }; + + return ( + + ); +} diff --git a/src/app/alert/[token]/page.tsx b/src/app/alert/[token]/page.tsx new file mode 100644 index 0000000..f91a909 --- /dev/null +++ b/src/app/alert/[token]/page.tsx @@ -0,0 +1,108 @@ +import { notFound } from "next/navigation"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; +import { BellRing, ShieldCheck, XCircle } from "lucide-react"; +import { NotdienstAlertStatus } from "@prisma/client"; + +import { prisma } from "@/lib/prisma"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { AlertConfirmForm } from "./alert-confirm-form"; + +export const metadata = { title: "Notdienst Alarm · Kita-Planer" }; + +export default async function AlertPage({ + params, +}: { + params: Promise<{ token: string }>; +}) { + const { token } = await params; + + const alert = await prisma.notdienstAlert.findUnique({ + where: { confirmationToken: token }, + include: { + parentUser: true, + kita: true, + assignment: { include: { child: true } }, + }, + }); + + if (!alert) { + notFound(); + } + + const isPending = alert.status === NotdienstAlertStatus.PENDING; + const isConfirmed = alert.status === NotdienstAlertStatus.CONFIRMED; + + return ( +
+ + +
+ {isPending ? ( + + ) : isConfirmed ? ( + + ) : ( + + )} +
+ + {isPending + ? "Notdienst Einsatz!" + : isConfirmed + ? "Einsatz bestätigt" + : "Alarm abgebrochen"} + + + Kita {alert.kita.name} + +
+ +
+

Datum

+

+ {format(alert.assignment.date, "EEEE, dd. MMMM yyyy", { + locale: de, + })} +

+
+

Eingeteiltes Kind

+

+ {alert.assignment.child.firstName}{" "} + {alert.assignment.child.lastName} +

+
+ +
+ {isPending ? ( + <> +

+ Hallo {alert.parentUser.firstName}, eine Fachkraft hat sich + krankgemeldet und du bist heute für den Notdienst eingeteilt. + Bitte bestätige deinen Einsatz schnellstmöglich! +

+ + + ) : isConfirmed ? ( +

+ Danke, {alert.parentUser.firstName}! Dein Einsatz wurde + erfolgreich bestätigt. Die Kita rechnet mit dir. +

+ ) : ( +

+ Dieser Einsatz wurde vom Koordinator abgebrochen. Du musst nicht + einspringen. +

+ )} +
+ + +
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..53ef86a --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,4 @@ +import { handlers } from "@/auth"; + +// NextAuth v5: Die fertigen Handler werden direkt re-exportiert. +export const { GET, POST } = handlers; diff --git a/src/app/dashboard/actions.ts b/src/app/dashboard/actions.ts new file mode 100644 index 0000000..e934eb5 --- /dev/null +++ b/src/app/dashboard/actions.ts @@ -0,0 +1,125 @@ +"use server"; + +import crypto from "crypto"; +import { createElement } from "react"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; +import { revalidatePath } from "next/cache"; +import { NotdienstAlertStatus, UserRole } from "@prisma/client"; + +import { AlertEmail } from "@/emails/AlertEmail"; +import { requireRole } from "@/lib/auth-utils"; +import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail"; +import { prisma } from "@/lib/prisma"; + +const BASE_URL = + process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000"; + +export async function triggerAlertAction( + assignmentId: string, + parentUserId: string, +) { + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + const kitaId = session.user.kitaId!; + + try { + const [existing, assignment] = await Promise.all([ + // Prüfen ob es schon einen aktiven Alert für dieses Assignment gibt + prisma.notdienstAlert.findFirst({ + where: { assignmentId, kitaId }, + }), + prisma.notdienstAssignment.findFirst({ + where: { id: assignmentId, kitaId }, + include: { + child: { + select: { + id: true, + firstName: true, + lastName: true, + }, + }, + }, + }), + ]); + + if (existing) { + return { error: "Für diese Einteilung wurde bereits ein Alarm ausgelöst." }; + } + + if (!assignment) { + return { error: "Notdienst-Einteilung wurde nicht gefunden." }; + } + + if (!parentUserId) { + return { error: "Kein Elternteil für diesen Notdienst hinterlegt." }; + } + + const mailConfigError = getAppEmailConfigError(); + if (mailConfigError) { + return { error: mailConfigError }; + } + + const parentLink = await prisma.childParent.findFirst({ + where: { + kitaId, + childId: assignment.child.id, + userId: parentUserId, + }, + include: { + user: { + select: { + email: true, + firstName: true, + lastName: true, + }, + }, + }, + }); + + if (!parentLink) { + return { error: "Das Elternteil passt nicht zu dieser Einteilung." }; + } + + const token = crypto.randomUUID(); + + await prisma.notdienstAlert.create({ + data: { + kitaId, + assignmentId, + parentUserId, + triggeredById: session.user.id, + status: NotdienstAlertStatus.PENDING, + confirmationToken: token, + }, + }); + + const alertUrl = `${BASE_URL}/alert/${token}`; + const dateLabel = format(assignment.date, "EEEE, dd. MMMM yyyy", { + locale: de, + }); + const childName = `${assignment.child.firstName} ${assignment.child.lastName}`; + + const emailResult = await sendAppEmail({ + to: parentLink.user.email, + subject: `Dringender Notdienst-Alarm für ${dateLabel}`, + react: createElement(AlertEmail, { + date: dateLabel, + childName, + confirmLink: alertUrl, + }), + }); + + revalidatePath("/dashboard"); + + if (!emailResult.success) { + return { + error: `Alarm wurde angelegt, aber die E-Mail konnte nicht versendet werden: ${emailResult.error}`, + }; + } + + return { success: true }; + } catch (error) { + console.error(error); + return { error: "Fehler beim Auslösen des Alarms." }; + } +} diff --git a/src/app/dashboard/adressbuch/page.tsx b/src/app/dashboard/adressbuch/page.tsx new file mode 100644 index 0000000..362308b --- /dev/null +++ b/src/app/dashboard/adressbuch/page.tsx @@ -0,0 +1,106 @@ +import { requireKitaSession } from "@/lib/auth-utils"; +import { prisma } from "@/lib/prisma"; +import { Contact, Mail, Phone, Baby } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; + +export default async function AdressbuchPage() { + const session = await requireKitaSession(); + + // Fetch only users who opted in to the directory + const users = await prisma.user.findMany({ + where: { + kitaId: session.user.kitaId, + directoryOptInAt: { not: null }, + }, + include: { + childLinks: { + include: { child: true }, + }, + dutyAssignments: { + include: { duty: true }, + }, + }, + orderBy: { lastName: "asc" }, + }); + + return ( +
+
+

Adressbuch

+

+ Kontaktinformationen aller Eltern, die der Freigabe zugestimmt haben. +

+
+ + {users.length === 0 ? ( +
+ +

Keine Kontakte

+

+ Bisher hat niemand der Veröffentlichung im Adressbuch zugestimmt. +

+
+ ) : ( +
+ {users.map((u) => ( + + + + + {u.firstName} {u.lastName} + + {u.role === "ADMIN" || u.role === "KOORDINATOR" ? ( + Vorstand + ) : null} + + + + + {u.phone && ( + + )} + + {u.childLinks.length > 0 && ( +
+ +
+ {u.childLinks.map(link => ( + + {link.child.firstName} + + ))} +
+
+ )} + + {u.dutyAssignments.length > 0 && ( +
+ Ämter / Dienste: +
+ {u.dutyAssignments.map((assignment) => ( + + {assignment.duty.name} + + ))} +
+
+ )} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/src/app/dashboard/alert-button.tsx b/src/app/dashboard/alert-button.tsx new file mode 100644 index 0000000..137881f --- /dev/null +++ b/src/app/dashboard/alert-button.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { useTransition } from "react"; +import { Loader2, BellRing } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { triggerAlertAction } from "./actions"; + +export function AlertButton({ + assignmentId, + parentUserId, +}: { + assignmentId: string; + parentUserId: string; +}) { + const [isPending, startTransition] = useTransition(); + + const handleAlert = () => { + if (!confirm("Bist du sicher? Dies löst den Notdienst-Alarm aus.")) return; + + startTransition(async () => { + const result = await triggerAlertAction(assignmentId, parentUserId); + if ("error" in result && result.error) { + toast.error(result.error); + } else { + toast.success("Alarm erfolgreich ausgelöst. E-Mail wurde versendet."); + } + }); + }; + + return ( + + ); +} diff --git a/src/app/dashboard/erzieher/_components/erzieher-list.tsx b/src/app/dashboard/erzieher/_components/erzieher-list.tsx new file mode 100644 index 0000000..de177d8 --- /dev/null +++ b/src/app/dashboard/erzieher/_components/erzieher-list.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Educator } from "@prisma/client"; +import { Plus, Pencil, Trash2 } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { createEducator, updateEducator, deleteEducator } from "../actions"; + +export function ErzieherList({ educators }: { educators: Educator[] }) { + const [openCreate, setOpenCreate] = useState(false); + const [editingEducator, setEditingEducator] = useState(null); + const [isPending, startTransition] = useTransition(); + + const handleCreate = (formData: FormData) => { + const data = { + firstName: formData.get("firstName") as string, + lastName: formData.get("lastName") as string, + active: formData.get("active") === "on", + }; + + startTransition(async () => { + const res = await createEducator(data); + if (res.error) { + toast.error(res.error); + } else { + toast.success("ErzieherIn angelegt."); + setOpenCreate(false); + } + }); + }; + + const handleUpdate = (formData: FormData) => { + if (!editingEducator) return; + const data = { + firstName: formData.get("firstName") as string, + lastName: formData.get("lastName") as string, + active: formData.get("active") === "on", + }; + + startTransition(async () => { + const res = await updateEducator(editingEducator.id, data); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Daten aktualisiert."); + setEditingEducator(null); + } + }); + }; + + const handleDelete = (id: string) => { + if (!confirm("Wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.")) return; + startTransition(async () => { + const res = await deleteEducator(id); + if (res.error) { + toast.error(res.error); + } else { + toast.success("ErzieherIn gelöscht."); + } + }); + }; + + return ( +
+
+ + + + + +
+ + Neue(n) ErzieherIn anlegen + + Erfasse die Stammdaten des Kita-Personals. + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+
+ +
+ + + + Vorname + Nachname + Status + Aktionen + + + + {educators.length === 0 ? ( + + + Keine Einträge gefunden. + + + ) : ( + educators.map((ed) => ( + + {ed.firstName} + {ed.lastName} + + + {ed.active ? "Aktiv" : "Inaktiv"} + + + + + + + + )) + )} + +
+
+ + {/* Edit Dialog */} + !open && setEditingEducator(null)}> + +
+ + ErzieherIn bearbeiten + + {editingEducator && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ )} + + + + +
+
+
+
+ ); +} diff --git a/src/app/dashboard/erzieher/actions.ts b/src/app/dashboard/erzieher/actions.ts new file mode 100644 index 0000000..a1dc1ef --- /dev/null +++ b/src/app/dashboard/erzieher/actions.ts @@ -0,0 +1,88 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { UserRole } from "@prisma/client"; +import { z } from "zod"; + +import { requireRole } from "@/lib/auth-utils"; +import { prisma } from "@/lib/prisma"; + +const educatorSchema = z.object({ + firstName: z.string().min(1, "Vorname ist erforderlich"), + lastName: z.string().min(1, "Nachname ist erforderlich"), + active: z.boolean().default(true), +}); + +export async function createEducator(rawPayload: unknown) { + const session = await requireRole([UserRole.ADMIN]); + + const parsed = educatorSchema.safeParse(rawPayload); + if (!parsed.success) { + return { error: "Ungültige Eingabedaten." }; + } + + try { + await prisma.educator.create({ + data: { + kitaId: session.user.kitaId!, + firstName: parsed.data.firstName, + lastName: parsed.data.lastName, + active: parsed.data.active, + }, + }); + + revalidatePath("/dashboard/erzieher"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Anlegen:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} + +export async function updateEducator(id: string, rawPayload: unknown) { + const session = await requireRole([UserRole.ADMIN]); + + const parsed = educatorSchema.safeParse(rawPayload); + if (!parsed.success) { + return { error: "Ungültige Eingabedaten." }; + } + + try { + await prisma.educator.update({ + where: { + id, + kitaId: session.user.kitaId!, + }, + data: { + firstName: parsed.data.firstName, + lastName: parsed.data.lastName, + active: parsed.data.active, + }, + }); + + revalidatePath("/dashboard/erzieher"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Aktualisieren:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} + +export async function deleteEducator(id: string) { + const session = await requireRole([UserRole.ADMIN]); + + try { + await prisma.educator.delete({ + where: { + id, + kitaId: session.user.kitaId!, + }, + }); + + revalidatePath("/dashboard/erzieher"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Löschen:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} diff --git a/src/app/dashboard/erzieher/page.tsx b/src/app/dashboard/erzieher/page.tsx new file mode 100644 index 0000000..8bf8df2 --- /dev/null +++ b/src/app/dashboard/erzieher/page.tsx @@ -0,0 +1,29 @@ +import { UserRole } from "@prisma/client"; + +import { requireRole } from "@/lib/auth-utils"; +import { prisma } from "@/lib/prisma"; +import { ErzieherList } from "./_components/erzieher-list"; + +export const metadata = { title: "ErzieherInnen · Kita-Planer" }; + +export default async function ErzieherPage() { + const session = await requireRole([UserRole.ADMIN]); + + const educators = await prisma.educator.findMany({ + where: { kitaId: session.user.kitaId! }, + orderBy: [{ lastName: "asc" }, { firstName: "asc" }], + }); + + return ( +
+
+

ErzieherInnen-Verwaltung

+

+ Verwalte die Stammdaten des Kita-Personals (nur für den Vorstand sichtbar). +

+
+ + +
+ ); +} diff --git a/src/app/dashboard/families/actions.ts b/src/app/dashboard/families/actions.ts new file mode 100644 index 0000000..b747c5b --- /dev/null +++ b/src/app/dashboard/families/actions.ts @@ -0,0 +1,194 @@ +"use server"; + +import crypto from "crypto"; +import { createElement } from "react"; +import { Prisma, UserRole } from "@prisma/client"; +import { z } from "zod"; + +import { prisma } from "@/lib/prisma"; +import { requireRole } from "@/lib/auth-utils"; +import { InviteEmail } from "@/emails/InviteEmail"; +import { getAppEmailConfigError, sendAppEmail } from "@/lib/mail"; + +// ===================================================================== +// /dashboard/families · Server Actions +// --------------------------------------------------------------------- +// addFamilyAction: Erstellt Elternteil + Kinder + VerificationToken in +// einer atomaren Prisma-Transaktion. Die kitaId kommt ausschließlich +// aus der validierten Session — nie aus dem Formular-Input. +// ===================================================================== + +const parentSchema = z.object({ + firstName: z.string().min(1, "Pflichtfeld.").max(100).trim(), + lastName: z.string().min(1, "Pflichtfeld.").max(100).trim(), + email: z + .string() + .email("Bitte eine gültige E-Mail-Adresse angeben.") + .toLowerCase() + .trim(), + childCount: z.coerce + .number() + .int() + .min(1, "Mindestens ein Kind erforderlich.") + .max(10), +}); + +const childSchema = z.object({ + firstName: z.string().min(1, "Vorname des Kindes fehlt.").max(100).trim(), + lastName: z.string().min(1, "Nachname des Kindes fehlt.").max(100).trim(), +}); + +export type AddFamilyState = { + errors?: { + firstName?: string[]; + lastName?: string[]; + email?: string[]; + children?: string[]; + _form?: string[]; + }; + success?: boolean; +}; + +const PRIVACY_POLICY_VERSION = "2026-05-01"; +const INVITE_TOKEN_TTL_DAYS = 7; +const BASE_URL = + process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000"; + +export async function addFamilyAction( + _prev: AddFamilyState, + formData: FormData, +): Promise { + // ── 1. Nur Admins dürfen Familien anlegen ────────────────────────── + const session = await requireRole([UserRole.ADMIN, UserRole.SUPERADMIN]); + + // ── 2. Parent-Felder validieren ──────────────────────────────────── + const parsedParent = parentSchema.safeParse(Object.fromEntries(formData)); + if (!parsedParent.success) { + return { errors: parsedParent.error.flatten().fieldErrors }; + } + const { firstName, lastName, email, childCount } = parsedParent.data; + + // ── 3. Kinder-Felder validieren ──────────────────────────────────── + const childrenRaw: { firstName: string; lastName: string }[] = []; + for (let i = 0; i < childCount; i++) { + const parsed = childSchema.safeParse({ + firstName: formData.get(`childFirstName_${i}`), + lastName: formData.get(`childLastName_${i}`), + }); + if (!parsed.success) { + return { + errors: { children: [`Kind ${i + 1}: ${Object.values(parsed.error.flatten().fieldErrors).flat().join(", ")}`] }, + }; + } + childrenRaw.push(parsed.data); + } + + // ── 4. Datenbank-Transaktion ─────────────────────────────────────── + // kitaId kommt ausschließlich aus der Session (Mandanten-Isolation!). + // SUPERADMIN hat keine kitaId → dieser Pfad sollte nie erreicht werden, + // aber wir prüfen explizit, um den Typ zu narrowen. + const kitaId = session.user.kitaId; + if (!kitaId) { + return { errors: { _form: ["Kein Mandant zugeordnet."] } }; + } + + const mailConfigError = getAppEmailConfigError(); + if (mailConfigError) { + return { errors: { _form: [mailConfigError] } }; + } + + const kita = await prisma.kita.findUnique({ + where: { id: kitaId }, + select: { name: true }, + }); + + if (!kita) { + return { errors: { _form: ["Kita wurde nicht gefunden."] } }; + } + + const parentName = `${firstName} ${lastName}`; + const token = crypto.randomUUID(); + const inviteUrl = `${BASE_URL}/invite/${token}`; + const expires = new Date( + Date.now() + INVITE_TOKEN_TTL_DAYS * 24 * 60 * 60_000, + ); + + try { + await prisma.$transaction(async (tx) => { + // 4a. Elternteil anlegen (kein Passwort → leerer passwordHash) + const parent = await tx.user.create({ + data: { + email, + firstName, + lastName, + passwordHash: "", // wird beim Invite-Einlösen gesetzt + role: UserRole.ELTERN, + kitaId, + }, + }); + + // 4b. Kinder anlegen + mit Elternteil verknüpfen + for (const child of childrenRaw) { + const createdChild = await tx.child.create({ + data: { + kitaId, + firstName: child.firstName, + lastName: child.lastName, + }, + }); + + await tx.childParent.create({ + data: { + kitaId, + childId: createdChild.id, + userId: parent.id, + }, + }); + } + + // 4c. Einladungs-Token erstellen + // identifier = userId (kein PII im Token selbst) + await tx.verificationToken.create({ + data: { + identifier: parent.id, + token, + expires, + }, + }); + }); + } catch (err) { + if ( + err instanceof Prisma.PrismaClientKnownRequestError && + err.code === "P2002" + ) { + return { + errors: { + email: ["Mit dieser E-Mail-Adresse existiert bereits ein Account."], + }, + }; + } + throw err; + } + + const emailResult = await sendAppEmail({ + to: email, + subject: `Einladung zu ${kita.name} im Kita-Planer`, + react: createElement(InviteEmail, { + parentName, + kitaName: kita.name, + inviteLink: inviteUrl, + }), + }); + + if (!emailResult.success) { + return { + errors: { + _form: [ + `Familie wurde angelegt, aber die Einladung konnte nicht versendet werden: ${emailResult.error}`, + ], + }, + }; + } + + return { success: true }; +} diff --git a/src/app/dashboard/families/add-family-dialog.tsx b/src/app/dashboard/families/add-family-dialog.tsx new file mode 100644 index 0000000..66fc4d4 --- /dev/null +++ b/src/app/dashboard/families/add-family-dialog.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useActionState, useState } from "react"; +import { Plus, Trash2 } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { addFamilyAction, type AddFamilyState } from "./actions"; + +const initialState: AddFamilyState = {}; + +// ===================================================================== +// AddFamilyDialog · Client Component +// --------------------------------------------------------------------- +// Verwaltet: +// • Dialog-Open/Close-State +// • Dynamische Kinderliste (min. 1, max. 10) +// • useActionState → Server-Action-Fehler anzeigen +// • Bei Erfolg (state.success) Dialog automatisch schließen +// ===================================================================== + +export function AddFamilyDialog() { + const [open, setOpen] = useState(false); + const [childCount, setChildCount] = useState(1); + const [state, formAction, pending] = useActionState( + addFamilyAction, + initialState, + ); + + // Dialog schließen, wenn die Action erfolgreich war + if (state.success && open) { + setOpen(false); + } + + function handleOpenChange(nextOpen: boolean) { + setOpen(nextOpen); + // State beim Schließen zurücksetzen würde useActionState erfordern — + // stattdessen schließen wir einfach und der State bleibt bis zur nächsten + // Aktion erhalten (unkritisch). + } + + return ( + + + + + + + + Familie hinzufügen + + Lege das Elternteil und die zugehörigen Kinder an. Der Elternteil + erhält einen Einladungslink per E-Mail, um sein Passwort zu setzen. + + + +
+ {/* Anzahl Kinder als verstecktes Feld für die Server Action */} + + + {/* ── Elternteil ─────────────────────────────────────────── */} +
+ Elternteil + +
+ + +
+ + +
+ + {/* ── Kinder ─────────────────────────────────────────────── */} +
+
+ + Kinder ({childCount}) + + {childCount < 10 && ( + + )} +
+ + {Array.from({ length: childCount }).map((_, i) => ( +
+ {childCount > 1 && ( + + )} +

+ Kind {i + 1} +

+
+ + +
+
+ ))} + + {state.errors?.children?.[0] && ( +

+ {state.errors.children[0]} +

+ )} +
+ + {/* Globaler Fehler */} + {state.errors?._form?.[0] && ( +

+ {state.errors._form[0]} +

+ )} + + + + + +
+
+
+ ); +} + +// ── Hilfskomponente ──────────────────────────────────────────────────── + +function FormField({ + id, + name, + label, + type = "text", + autoComplete, + error, +}: { + id: string; + name: string; + label: string; + type?: string; + autoComplete?: string; + error?: string; +}) { + return ( +
+ + + {error &&

{error}

} +
+ ); +} diff --git a/src/app/dashboard/families/duty-actions.ts b/src/app/dashboard/families/duty-actions.ts new file mode 100644 index 0000000..c5b7288 --- /dev/null +++ b/src/app/dashboard/families/duty-actions.ts @@ -0,0 +1,109 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { UserRole } from "@prisma/client"; +import { z } from "zod"; + +import { requireRole } from "@/lib/auth-utils"; +import { prisma } from "@/lib/prisma"; + +const dutySchema = z.object({ + name: z.string().min(2, "Name muss mindestens 2 Zeichen lang sein").max(50), + description: z.string().optional(), +}); + +export async function createDuty(rawPayload: unknown) { + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + const parsed = dutySchema.safeParse(rawPayload); + + if (!parsed.success) { + return { error: "Ungültige Eingabedaten." }; + } + + try { + await prisma.parentDuty.create({ + data: { + kitaId: session.user.kitaId!, + name: parsed.data.name, + description: parsed.data.description, + }, + }); + revalidatePath("/dashboard/families"); + revalidatePath("/dashboard/adressbuch"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Erstellen des Amtes:", error); + return { error: "Ein Fehler ist aufgetreten (evtl. existiert der Name bereits)." }; + } +} + +export async function deleteDuty(dutyId: string) { + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + + try { + await prisma.parentDuty.delete({ + where: { + id: dutyId, + kitaId: session.user.kitaId!, + }, + }); + revalidatePath("/dashboard/families"); + revalidatePath("/dashboard/adressbuch"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Löschen des Amtes:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} + +export async function assignDuty(userId: string, dutyId: string) { + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + + try { + // Check if assignment already exists + const existing = await prisma.parentDutyAssignment.findUnique({ + where: { + dutyId_userId: { dutyId, userId }, + }, + }); + + if (existing) { + return { error: "Der Nutzer hat dieses Amt bereits." }; + } + + await prisma.parentDutyAssignment.create({ + data: { + kitaId: session.user.kitaId!, + userId: userId, + dutyId: dutyId, + }, + }); + + revalidatePath("/dashboard/families"); + revalidatePath("/dashboard/adressbuch"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Zuweisen des Amtes:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} + +export async function removeDutyAssignment(assignmentId: string) { + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + + try { + await prisma.parentDutyAssignment.delete({ + where: { + id: assignmentId, + kitaId: session.user.kitaId!, + }, + }); + + revalidatePath("/dashboard/families"); + revalidatePath("/dashboard/adressbuch"); + return { success: true }; + } catch (error) { + console.error("Fehler beim Entfernen des Amtes:", error); + return { error: "Ein Fehler ist aufgetreten." }; + } +} diff --git a/src/app/dashboard/families/duty-manager.tsx b/src/app/dashboard/families/duty-manager.tsx new file mode 100644 index 0000000..aabeb22 --- /dev/null +++ b/src/app/dashboard/families/duty-manager.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { Plus, X, Shield, Trash2 } from "lucide-react"; +import { ParentDuty, ParentDutyAssignment, User } from "@prisma/client"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; + +import { createDuty, assignDuty, removeDutyAssignment, deleteDuty } from "./duty-actions"; + +type DutyWithAssignments = ParentDuty & { + assignments: ParentDutyAssignment[]; +}; + +export function DutyManager({ + user, + allDuties, + userAssignments, +}: { + user: User; + allDuties: DutyWithAssignments[]; + userAssignments: (ParentDutyAssignment & { duty: ParentDuty })[]; +}) { + const [open, setOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + const handleCreateDuty = (formData: FormData) => { + const name = formData.get("name") as string; + if (!name) return; + + startTransition(async () => { + const res = await createDuty({ name }); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Amt erstellt."); + } + }); + }; + + const handleAssign = (dutyId: string) => { + startTransition(async () => { + const res = await assignDuty(user.id, dutyId); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Amt zugewiesen."); + } + }); + }; + + const handleRemove = (assignmentId: string) => { + startTransition(async () => { + const res = await removeDutyAssignment(assignmentId); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Zuweisung entfernt."); + } + }); + }; + + const handleDeleteDuty = (dutyId: string) => { + startTransition(async () => { + const res = await deleteDuty(dutyId); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Amt komplett gelöscht."); + } + }); + }; + + const unassignedDuties = allDuties.filter( + (d) => !userAssignments.some((a) => a.dutyId === d.id) + ); + + return ( + + + + + + + Ämter verwalten + + Ämter und Dienste für {user.firstName} {user.lastName}. + + + +
+ {/* Current Assignments */} +
+

Aktuelle Ämter

+ {userAssignments.length === 0 ? ( +

Keine Ämter zugewiesen.

+ ) : ( +
+ {userAssignments.map((assignment) => ( + + {assignment.duty.name} + + + ))} +
+ )} +
+ + {/* Assign new duty */} +
+

Vorhandenes Amt zuweisen

+ {unassignedDuties.length === 0 ? ( +

Keine weiteren Ämter verfügbar.

+ ) : ( +
+ {unassignedDuties.map((duty) => ( + handleAssign(duty.id)} + > + + {duty.name} + + {/* Only show delete if no one is assigned to it across the entire DB */} + {duty.assignments.length === 0 && ( + + )} + + ))} +
+ )} +
+ + {/* Create new duty */} +
+

Neues Amt anlegen

+
+ + +
+
+
+
+
+ ); +} diff --git a/src/app/dashboard/families/page.tsx b/src/app/dashboard/families/page.tsx new file mode 100644 index 0000000..45efd1c --- /dev/null +++ b/src/app/dashboard/families/page.tsx @@ -0,0 +1,173 @@ +import { UserRole } from "@prisma/client"; + +import { requireRole } from "@/lib/auth-utils"; +import { prisma } from "@/lib/prisma"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { AddFamilyDialog } from "./add-family-dialog"; +import { DutyManager } from "./duty-manager"; + +export const metadata = { title: "Familienverwaltung · Kita-Planer" }; + +// ===================================================================== +// /dashboard/families · Server Component +// --------------------------------------------------------------------- +// Zeigt alle Elternteile (ELTERN) und deren Kinder für die aktuelle +// kitaId. Tenant-Filter ist garantiert: kitaId aus requireRole-Session. +// ===================================================================== + +export default async function FamiliesPage() { + // Guard: Nur Admins (und Koordinatoren) dürfen diese Seite sehen + const session = await requireRole([UserRole.ADMIN, UserRole.KOORDINATOR]); + + // Alle ELTERN-User der Kita mit ihren verknüpften Kindern laden + // kitaId IMMER aus der Session — nie aus URL/Params + const families = await prisma.user.findMany({ + where: { + kitaId: session.user.kitaId!, + role: UserRole.ELTERN, + }, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + passwordHash: true, // "" → Invite ausstehend; sonst aktiv + emailVerifiedAt: true, + childLinks: { + select: { + child: { + select: { id: true, firstName: true, lastName: true }, + }, + }, + }, + dutyAssignments: { + include: { duty: true }, + }, + }, + orderBy: [{ lastName: "asc" }, { firstName: "asc" }], + }); + + const allDuties = await prisma.parentDuty.findMany({ + where: { kitaId: session.user.kitaId! }, + include: { assignments: true }, + orderBy: { name: "asc" }, + }); + + return ( +
+ {/* Header */} +
+
+

+ Familienverwaltung +

+

+ {families.length === 0 + ? "Noch keine Familien angelegt." + : `${families.length} ${families.length === 1 ? "Familie" : "Familien"} · ${families.reduce((acc, f) => acc + f.childLinks.length, 0)} Kinder`} +

+
+ +
+ + {families.length === 0 ? ( + + ) : ( +
+ + + + Elternteil + E-Mail + Kinder + Status + Aktionen + + + + {families.map((family) => { + const isActive = !!family.emailVerifiedAt; + const children = family.childLinks.map((l) => l.child); + + return ( + + + {family.firstName} {family.lastName} + + + {family.email} + + + {children.length === 0 ? ( + + ) : ( +
+ {children.map((child) => ( + + {child.firstName} {child.lastName} + + ))} +
+ )} +
+ + + {isActive ? "Aktiv" : "Eingeladen"} + + + + + +
+ ); + })} +
+
+
+ )} +
+ ); +} + +function EmptyState() { + return ( +
+
+ + + +
+

Noch keine Familien

+

+ Lege die erste Familie an und lade das Elternteil per Link ein, sein + Passwort zu setzen. +

+ +
+ ); +} diff --git a/src/app/dashboard/kalender/_components/admin-termin-modal.tsx b/src/app/dashboard/kalender/_components/admin-termin-modal.tsx new file mode 100644 index 0000000..a7dd8ff --- /dev/null +++ b/src/app/dashboard/kalender/_components/admin-termin-modal.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { TerminType } from "@prisma/client"; +import { Plus } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { createTerminAdmin } from "../actions"; +import { Textarea } from "@/components/ui/textarea"; + +export function AdminTerminModal() { + const [open, setOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + const handleAction = (formData: FormData) => { + const data = { + title: formData.get("title") as string, + description: formData.get("description") as string, + type: formData.get("type") as TerminType, + startDate: new Date(formData.get("startDate") as string).toISOString(), + endDate: new Date(formData.get("endDate") as string).toISOString(), + allDay: formData.get("allDay") === "on", + }; + + startTransition(async () => { + const res = await createTerminAdmin(data); + if (res.error) { + toast.error(res.error); + } else { + toast.success("Termin wurde direkt angelegt!"); + setOpen(false); + } + }); + }; + + return ( + + + + + +
+ + Termin anlegen (Admin) + + Dieser Termin wird sofort freigegeben und ist für alle sichtbar. + + + +
+
+ + +
+ +
+ +