Add mantis summaries and start of email summaries... Need to figure out a way to get the emails since MS block IMAP :(
This commit is contained in:
parent
2d11d0bd79
commit
2ad9a63582
18 changed files with 1993 additions and 577 deletions
5
.env.example
Normal file
5
.env.example
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Add your environment variables here
|
||||||
|
GOOGLE_API_KEY=GOOGLE_API_KEY
|
||||||
|
MANTIS_API_KEY=MANTIS_API_KEY
|
||||||
|
MANTIS_API_ENDPOINT=https://styletech.mantishub.io/api/rest
|
||||||
|
DATABASE_URL="postgresql://sts-sls-utility:MY_SECURE_PASSWORD@localhost:5432/sts-sls-utility?schema=public"
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -30,4 +30,9 @@ yarn-error.log*
|
||||||
*.sln
|
*.sln
|
||||||
|
|
||||||
# local .env files
|
# local .env files
|
||||||
|
.env
|
||||||
.env.local*
|
.env.local*
|
||||||
|
|
||||||
|
/postgres
|
||||||
|
|
||||||
|
docker-compose.yml
|
17
docker-compose-example.yml
Normal file
17
docker-compose-example.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
services:
|
||||||
|
postgres_db:
|
||||||
|
image: postgres:latest # Use the latest official PostgreSQL image. Consider pinning to a specific version (e.g., postgres:15) for production.
|
||||||
|
container_name: sts_sls_utility_postgres # A friendly name for the container
|
||||||
|
restart: unless-stopped # Automatically restart the container unless it was manually stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: sts-sls-utility # Sets the default username as requested
|
||||||
|
POSTGRES_PASSWORD: MY_RANDOM_PASSWORD # Replace with a secure password
|
||||||
|
POSTGRES_DB: sts-sls-utility
|
||||||
|
volumes:
|
||||||
|
# Mounts the host directory './postgres' into the container's data directory
|
||||||
|
# This ensures data persists even if the container is removed and recreated.
|
||||||
|
- ./postgres:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
# Maps port 5432 on your host machine to port 5432 inside the container
|
||||||
|
# You can change the host port if 5432 is already in use (e.g., "5433:5432")
|
||||||
|
- "5432:5432"
|
10
package.json
10
package.json
|
@ -8,17 +8,22 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"No test specified\" && exit 0",
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
"dev": "quasar dev -m ssr",
|
"dev": "prisma db push && quasar dev -m ssr",
|
||||||
"build": "quasar build -m ssr",
|
"build": "quasar build -m ssr",
|
||||||
"postinstall": "quasar prepare"
|
"postinstall": "quasar prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^0.9.0",
|
"@google/genai": "^0.9.0",
|
||||||
|
"@prisma/client": "^6.6.0",
|
||||||
"@quasar/extras": "^1.16.4",
|
"@quasar/extras": "^1.16.4",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"better-sqlite3": "^11.9.1",
|
"better-sqlite3": "^11.9.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"mailparser": "^3.7.2",
|
||||||
|
"marked": "^15.0.9",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
|
"node-imap": "^0.9.6",
|
||||||
"pdfkit": "^0.17.0",
|
"pdfkit": "^0.17.0",
|
||||||
"pdfmake": "^0.2.18",
|
"pdfmake": "^0.2.18",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.16.0",
|
||||||
|
@ -28,7 +33,8 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^2.1.0",
|
"@quasar/app-vite": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"postcss": "^8.4.14"
|
"postcss": "^8.4.14",
|
||||||
|
"prisma": "^6.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
||||||
|
|
333
pnpm-lock.yaml
generated
333
pnpm-lock.yaml
generated
|
@ -11,6 +11,9 @@ importers:
|
||||||
'@google/genai':
|
'@google/genai':
|
||||||
specifier: ^0.9.0
|
specifier: ^0.9.0
|
||||||
version: 0.9.0
|
version: 0.9.0
|
||||||
|
'@prisma/client':
|
||||||
|
specifier: ^6.6.0
|
||||||
|
version: 6.6.0(prisma@6.6.0)
|
||||||
'@quasar/extras':
|
'@quasar/extras':
|
||||||
specifier: ^1.16.4
|
specifier: ^1.16.4
|
||||||
version: 1.16.17
|
version: 1.16.17
|
||||||
|
@ -26,6 +29,18 @@ importers:
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.5.0
|
specifier: ^16.5.0
|
||||||
version: 16.5.0
|
version: 16.5.0
|
||||||
|
mailparser:
|
||||||
|
specifier: ^3.7.2
|
||||||
|
version: 3.7.2
|
||||||
|
marked:
|
||||||
|
specifier: ^15.0.9
|
||||||
|
version: 15.0.9
|
||||||
|
node-cron:
|
||||||
|
specifier: ^3.0.3
|
||||||
|
version: 3.0.3
|
||||||
|
node-imap:
|
||||||
|
specifier: ^0.9.6
|
||||||
|
version: 0.9.6
|
||||||
pdfkit:
|
pdfkit:
|
||||||
specifier: ^0.17.0
|
specifier: ^0.17.0
|
||||||
version: 0.17.0
|
version: 0.17.0
|
||||||
|
@ -51,6 +66,9 @@ importers:
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.14
|
specifier: ^8.4.14
|
||||||
version: 8.5.3
|
version: 8.5.3
|
||||||
|
prisma:
|
||||||
|
specifier: ^6.6.0
|
||||||
|
version: 6.6.0
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
@ -273,6 +291,36 @@ packages:
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@prisma/client@6.6.0':
|
||||||
|
resolution: {integrity: sha512-vfp73YT/BHsWWOAuthKQ/1lBgESSqYqAWZEYyTdGXyFAHpmewwWL2Iz6ErIzkj4aHbuc6/cGSsE6ZY+pBO04Cg==}
|
||||||
|
engines: {node: '>=18.18'}
|
||||||
|
peerDependencies:
|
||||||
|
prisma: '*'
|
||||||
|
typescript: '>=5.1.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
prisma:
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@prisma/config@6.6.0':
|
||||||
|
resolution: {integrity: sha512-d8FlXRHsx72RbN8nA2QCRORNv5AcUnPXgtPvwhXmYkQSMF/j9cKaJg+9VcUzBRXGy9QBckNzEQDEJZdEOZ+ubA==}
|
||||||
|
|
||||||
|
'@prisma/debug@6.6.0':
|
||||||
|
resolution: {integrity: sha512-DL6n4IKlW5k2LEXzpN60SQ1kP/F6fqaCgU/McgaYsxSf43GZ8lwtmXLke9efS+L1uGmrhtBUP4npV/QKF8s2ZQ==}
|
||||||
|
|
||||||
|
'@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a':
|
||||||
|
resolution: {integrity: sha512-JzRaQ5Em1fuEcbR3nUsMNYaIYrOT1iMheenjCvzZblJcjv/3JIuxXN7RCNT5i6lRkLodW5ojCGhR7n5yvnNKrw==}
|
||||||
|
|
||||||
|
'@prisma/engines@6.6.0':
|
||||||
|
resolution: {integrity: sha512-nC0IV4NHh7500cozD1fBoTwTD1ydJERndreIjpZr/S3mno3P6tm8qnXmIND5SwUkibNeSJMpgl4gAnlqJ/gVlg==}
|
||||||
|
|
||||||
|
'@prisma/fetch-engine@6.6.0':
|
||||||
|
resolution: {integrity: sha512-Ohfo8gKp05LFLZaBlPUApM0M7k43a0jmo86YY35u1/4t+vuQH9mRGU7jGwVzGFY3v+9edeb/cowb1oG4buM1yw==}
|
||||||
|
|
||||||
|
'@prisma/get-platform@6.6.0':
|
||||||
|
resolution: {integrity: sha512-3qCwmnT4Jh5WCGUrkWcc6VZaw0JY7eWN175/pcb5Z6FiLZZ3ygY93UX0WuV41bG51a6JN/oBH0uywJ90Y+V5eA==}
|
||||||
|
|
||||||
'@quasar/app-vite@2.2.0':
|
'@quasar/app-vite@2.2.0':
|
||||||
resolution: {integrity: sha512-MvCfJrCbxUYvoGaK5jPq0h0hjO8mbxYOWngf+dIKrxhwb+1h5ERh6aVYEUuCtMIwTMEVfPkCez4DIfZIoReuDw==}
|
resolution: {integrity: sha512-MvCfJrCbxUYvoGaK5jPq0h0hjO8mbxYOWngf+dIKrxhwb+1h5ERh6aVYEUuCtMIwTMEVfPkCez4DIfZIoReuDw==}
|
||||||
engines: {node: ^30 || ^28 || ^26 || ^24 || ^22 || ^20 || ^18, npm: '>= 6.14.12', yarn: '>= 1.17.3'}
|
engines: {node: ^30 || ^28 || ^26 || ^24 || ^22 || ^20 || ^18, npm: '>= 6.14.12', yarn: '>= 1.17.3'}
|
||||||
|
@ -421,6 +469,9 @@ packages:
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
|
@ -842,6 +893,10 @@ packages:
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
deepmerge@4.3.1:
|
||||||
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
default-browser-id@5.0.0:
|
default-browser-id@5.0.0:
|
||||||
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
|
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
@ -888,6 +943,19 @@ packages:
|
||||||
dfa@1.2.0:
|
dfa@1.2.0:
|
||||||
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
|
domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
|
||||||
|
@ -937,6 +1005,10 @@ packages:
|
||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
encoding-japanese@2.2.0:
|
||||||
|
resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==}
|
||||||
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
end-of-stream@1.4.4:
|
end-of-stream@1.4.4:
|
||||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||||
|
|
||||||
|
@ -960,6 +1032,11 @@ packages:
|
||||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
esbuild-register@3.6.0:
|
||||||
|
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
|
||||||
|
peerDependencies:
|
||||||
|
esbuild: '>=0.12 <1'
|
||||||
|
|
||||||
esbuild@0.25.3:
|
esbuild@0.25.3:
|
||||||
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
|
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
@ -1149,11 +1226,22 @@ packages:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
he@1.2.0:
|
||||||
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
html-minifier-terser@7.2.0:
|
html-minifier-terser@7.2.0:
|
||||||
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
|
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
html-to-text@9.0.5:
|
||||||
|
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -1304,9 +1392,24 @@ packages:
|
||||||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||||
engines: {node: '>= 0.6.3'}
|
engines: {node: '>= 0.6.3'}
|
||||||
|
|
||||||
|
leac@0.6.0:
|
||||||
|
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||||
|
|
||||||
|
libbase64@1.3.0:
|
||||||
|
resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==}
|
||||||
|
|
||||||
|
libmime@5.3.6:
|
||||||
|
resolution: {integrity: sha512-j9mBC7eiqi6fgBPAGvKCXJKJSIASanYF4EeA4iBzSG0HxQxmXnR3KbyWqTn4CwsKSebqCv2f5XZfAO6sKzgvwA==}
|
||||||
|
|
||||||
|
libqp@2.1.1:
|
||||||
|
resolution: {integrity: sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==}
|
||||||
|
|
||||||
linebreak@1.1.0:
|
linebreak@1.1.0:
|
||||||
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
|
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
|
@ -1323,6 +1426,17 @@ packages:
|
||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||||
|
|
||||||
|
mailparser@3.7.2:
|
||||||
|
resolution: {integrity: sha512-iI0p2TCcIodR1qGiRoDBBwboSSff50vQAWytM5JRggLfABa4hHYCf3YVujtuzV454xrOP352VsAPIzviqMTo4Q==}
|
||||||
|
|
||||||
|
mailsplit@5.4.2:
|
||||||
|
resolution: {integrity: sha512-4cczG/3Iu3pyl8JgQ76dKkisurZTmxMrA4dj/e8d2jKYcFTZ7MxOzg1gTioTDMPuFXwTrVuN/gxhkrO7wLg7qA==}
|
||||||
|
|
||||||
|
marked@15.0.9:
|
||||||
|
resolution: {integrity: sha512-9AW/bn9DxQeZVjR52l5jsc0W2pwuhP04QaQewPvylil12Cfr2GBfWmgp6mu8i9Jy8UlBjqDZ9uMTDuJ8QOGZJA==}
|
||||||
|
engines: {node: '>= 18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -1417,6 +1531,10 @@ packages:
|
||||||
resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==}
|
resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
node-cron@3.0.3:
|
||||||
|
resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
@ -1430,9 +1548,17 @@ packages:
|
||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
|
|
||||||
|
node-imap@0.9.6:
|
||||||
|
resolution: {integrity: sha512-pYQ2AtjQwrSvILq8EYInv3E3svrJwrTOxzW7uBGpP//AkCs/pMdO+O6KEgUlSchh/0/N0MSWs5io3xZhxJ9yLg==}
|
||||||
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
node-releases@2.0.19:
|
node-releases@2.0.19:
|
||||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||||
|
|
||||||
|
nodemailer@6.9.16:
|
||||||
|
resolution: {integrity: sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -1493,6 +1619,9 @@ packages:
|
||||||
param-case@3.0.4:
|
param-case@3.0.4:
|
||||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||||
|
|
||||||
|
parseley@0.12.1:
|
||||||
|
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -1521,6 +1650,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Fe+GnMS8EVZu5rci/CDaQ+xmUoHvx8P+rvIlrwSYM6A5c7Aik8G6lpJbddhjBE2jXGjv6WcUCFCB06uZbjxkMw==}
|
resolution: {integrity: sha512-Fe+GnMS8EVZu5rci/CDaQ+xmUoHvx8P+rvIlrwSYM6A5c7Aik8G6lpJbddhjBE2jXGjv6WcUCFCB06uZbjxkMw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
peberminta@0.9.0:
|
||||||
|
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
|
@ -1550,6 +1682,16 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
prisma@6.6.0:
|
||||||
|
resolution: {integrity: sha512-SYCUykz+1cnl6Ugd8VUvtTQq5+j1Q7C0CtzKPjQ8JyA2ALh0EEJkMCS+KgdnvKW1lrxjtjCyJSHOOT236mENYg==}
|
||||||
|
engines: {node: '>=18.18'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
typescript: '>=5.1.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
|
@ -1567,6 +1709,10 @@ packages:
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
|
punycode.js@2.3.1:
|
||||||
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
@ -1796,10 +1942,17 @@ packages:
|
||||||
sax@1.4.1:
|
sax@1.4.1:
|
||||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||||
|
|
||||||
|
selderee@0.11.0:
|
||||||
|
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||||
|
|
||||||
selfsigned@2.4.1:
|
selfsigned@2.4.1:
|
||||||
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
|
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
semver@5.3.0:
|
||||||
|
resolution: {integrity: sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
semver@7.7.1:
|
semver@7.7.1:
|
||||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -1961,6 +2114,10 @@ packages:
|
||||||
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tlds@1.255.0:
|
||||||
|
resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tmp@0.0.33:
|
tmp@0.0.33:
|
||||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||||
engines: {node: '>=0.6.0'}
|
engines: {node: '>=0.6.0'}
|
||||||
|
@ -2002,6 +2159,9 @@ packages:
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
uc.micro@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||||
|
|
||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||||
|
|
||||||
|
@ -2028,6 +2188,9 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
|
utf7@1.0.2:
|
||||||
|
resolution: {integrity: sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
@ -2035,6 +2198,10 @@ packages:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
uuid@9.0.1:
|
uuid@9.0.1:
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -2344,6 +2511,38 @@ snapshots:
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@prisma/client@6.6.0(prisma@6.6.0)':
|
||||||
|
optionalDependencies:
|
||||||
|
prisma: 6.6.0
|
||||||
|
|
||||||
|
'@prisma/config@6.6.0':
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.25.3
|
||||||
|
esbuild-register: 3.6.0(esbuild@0.25.3)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@prisma/debug@6.6.0': {}
|
||||||
|
|
||||||
|
'@prisma/engines-version@6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a': {}
|
||||||
|
|
||||||
|
'@prisma/engines@6.6.0':
|
||||||
|
dependencies:
|
||||||
|
'@prisma/debug': 6.6.0
|
||||||
|
'@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
|
||||||
|
'@prisma/fetch-engine': 6.6.0
|
||||||
|
'@prisma/get-platform': 6.6.0
|
||||||
|
|
||||||
|
'@prisma/fetch-engine@6.6.0':
|
||||||
|
dependencies:
|
||||||
|
'@prisma/debug': 6.6.0
|
||||||
|
'@prisma/engines-version': 6.6.0-53.f676762280b54cd07c770017ed3711ddde35f37a
|
||||||
|
'@prisma/get-platform': 6.6.0
|
||||||
|
|
||||||
|
'@prisma/get-platform@6.6.0':
|
||||||
|
dependencies:
|
||||||
|
'@prisma/debug': 6.6.0
|
||||||
|
|
||||||
'@quasar/app-vite@2.2.0(@types/node@22.14.1)(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)':
|
'@quasar/app-vite@2.2.0(@types/node@22.14.1)(quasar@2.18.1)(rollup@4.40.0)(terser@5.39.0)(vue-router@4.5.0(vue@3.5.13))(vue@3.5.13)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@quasar/render-ssr-error': 1.0.3
|
'@quasar/render-ssr-error': 1.0.3
|
||||||
|
@ -2479,6 +2678,11 @@ snapshots:
|
||||||
'@rollup/rollup-win32-x64-msvc@4.40.0':
|
'@rollup/rollup-win32-x64-msvc@4.40.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@selderee/plugin-htmlparser2@0.11.0':
|
||||||
|
dependencies:
|
||||||
|
domhandler: 5.0.3
|
||||||
|
selderee: 0.11.0
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
@ -2954,6 +3158,8 @@ snapshots:
|
||||||
|
|
||||||
deep-extend@0.6.0: {}
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@5.0.0: {}
|
default-browser-id@5.0.0: {}
|
||||||
|
|
||||||
default-browser@5.2.1:
|
default-browser@5.2.1:
|
||||||
|
@ -2991,6 +3197,24 @@ snapshots:
|
||||||
|
|
||||||
dfa@1.2.0: {}
|
dfa@1.2.0: {}
|
||||||
|
|
||||||
|
dom-serializer@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
|
domelementtype@2.3.0: {}
|
||||||
|
|
||||||
|
domhandler@5.0.3:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
domutils@3.2.2:
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
|
||||||
dot-case@3.0.4:
|
dot-case@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
no-case: 3.0.4
|
no-case: 3.0.4
|
||||||
|
@ -3034,6 +3258,8 @@ snapshots:
|
||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
encoding-japanese@2.2.0: {}
|
||||||
|
|
||||||
end-of-stream@1.4.4:
|
end-of-stream@1.4.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
@ -3055,6 +3281,13 @@ snapshots:
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
|
|
||||||
|
esbuild-register@3.6.0(esbuild@0.25.3):
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.0
|
||||||
|
esbuild: 0.25.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
esbuild@0.25.3:
|
esbuild@0.25.3:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@esbuild/aix-ppc64': 0.25.3
|
'@esbuild/aix-ppc64': 0.25.3
|
||||||
|
@ -3313,6 +3546,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
he@1.2.0: {}
|
||||||
|
|
||||||
html-minifier-terser@7.2.0:
|
html-minifier-terser@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
camel-case: 4.1.2
|
camel-case: 4.1.2
|
||||||
|
@ -3323,6 +3558,21 @@ snapshots:
|
||||||
relateurl: 0.2.7
|
relateurl: 0.2.7
|
||||||
terser: 5.39.0
|
terser: 5.39.0
|
||||||
|
|
||||||
|
html-to-text@9.0.5:
|
||||||
|
dependencies:
|
||||||
|
'@selderee/plugin-htmlparser2': 0.11.0
|
||||||
|
deepmerge: 4.3.1
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
selderee: 0.11.0
|
||||||
|
|
||||||
|
htmlparser2@8.0.2:
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.2.2
|
||||||
|
entities: 4.5.0
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
|
@ -3473,11 +3723,28 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
|
|
||||||
|
leac@0.6.0: {}
|
||||||
|
|
||||||
|
libbase64@1.3.0: {}
|
||||||
|
|
||||||
|
libmime@5.3.6:
|
||||||
|
dependencies:
|
||||||
|
encoding-japanese: 2.2.0
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
libbase64: 1.3.0
|
||||||
|
libqp: 2.1.1
|
||||||
|
|
||||||
|
libqp@2.1.1: {}
|
||||||
|
|
||||||
linebreak@1.1.0:
|
linebreak@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 0.0.8
|
base64-js: 0.0.8
|
||||||
unicode-trie: 2.0.0
|
unicode-trie: 2.0.0
|
||||||
|
|
||||||
|
linkify-it@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
log-symbols@4.1.0:
|
log-symbols@4.1.0:
|
||||||
|
@ -3495,6 +3762,27 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
|
mailparser@3.7.2:
|
||||||
|
dependencies:
|
||||||
|
encoding-japanese: 2.2.0
|
||||||
|
he: 1.2.0
|
||||||
|
html-to-text: 9.0.5
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
libmime: 5.3.6
|
||||||
|
linkify-it: 5.0.0
|
||||||
|
mailsplit: 5.4.2
|
||||||
|
nodemailer: 6.9.16
|
||||||
|
punycode.js: 2.3.1
|
||||||
|
tlds: 1.255.0
|
||||||
|
|
||||||
|
mailsplit@5.4.2:
|
||||||
|
dependencies:
|
||||||
|
libbase64: 1.3.0
|
||||||
|
libmime: 5.3.6
|
||||||
|
libqp: 2.1.1
|
||||||
|
|
||||||
|
marked@15.0.9: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
@ -3561,14 +3849,25 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.1
|
semver: 7.7.1
|
||||||
|
|
||||||
|
node-cron@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
uuid: 8.3.2
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-forge@1.3.1: {}
|
node-forge@1.3.1: {}
|
||||||
|
|
||||||
|
node-imap@0.9.6:
|
||||||
|
dependencies:
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
utf7: 1.0.2
|
||||||
|
|
||||||
node-releases@2.0.19: {}
|
node-releases@2.0.19: {}
|
||||||
|
|
||||||
|
nodemailer@6.9.16: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
|
@ -3632,6 +3931,11 @@ snapshots:
|
||||||
dot-case: 3.0.4
|
dot-case: 3.0.4
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
parseley@0.12.1:
|
||||||
|
dependencies:
|
||||||
|
leac: 0.6.0
|
||||||
|
peberminta: 0.9.0
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
pascal-case@3.1.2:
|
pascal-case@3.1.2:
|
||||||
|
@ -3665,6 +3969,8 @@ snapshots:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
xmldoc: 1.3.0
|
xmldoc: 1.3.0
|
||||||
|
|
||||||
|
peberminta@0.9.0: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@2.3.1: {}
|
picomatch@2.3.1: {}
|
||||||
|
@ -3702,6 +4008,15 @@ snapshots:
|
||||||
tar-fs: 2.1.2
|
tar-fs: 2.1.2
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
|
prisma@6.6.0:
|
||||||
|
dependencies:
|
||||||
|
'@prisma/config': 6.6.0
|
||||||
|
'@prisma/engines': 6.6.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
|
@ -3718,6 +4033,8 @@ snapshots:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
once: 1.4.0
|
once: 1.4.0
|
||||||
|
|
||||||
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
qs@6.13.0:
|
qs@6.13.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
@ -3941,11 +4258,17 @@ snapshots:
|
||||||
|
|
||||||
sax@1.4.1: {}
|
sax@1.4.1: {}
|
||||||
|
|
||||||
|
selderee@0.11.0:
|
||||||
|
dependencies:
|
||||||
|
parseley: 0.12.1
|
||||||
|
|
||||||
selfsigned@2.4.1:
|
selfsigned@2.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node-forge': 1.3.11
|
'@types/node-forge': 1.3.11
|
||||||
node-forge: 1.3.1
|
node-forge: 1.3.1
|
||||||
|
|
||||||
|
semver@5.3.0: {}
|
||||||
|
|
||||||
semver@7.7.1: {}
|
semver@7.7.1: {}
|
||||||
|
|
||||||
send@0.19.0:
|
send@0.19.0:
|
||||||
|
@ -4152,6 +4475,8 @@ snapshots:
|
||||||
fdir: 6.4.4(picomatch@4.0.2)
|
fdir: 6.4.4(picomatch@4.0.2)
|
||||||
picomatch: 4.0.2
|
picomatch: 4.0.2
|
||||||
|
|
||||||
|
tlds@1.255.0: {}
|
||||||
|
|
||||||
tmp@0.0.33:
|
tmp@0.0.33:
|
||||||
dependencies:
|
dependencies:
|
||||||
os-tmpdir: 1.0.2
|
os-tmpdir: 1.0.2
|
||||||
|
@ -4181,6 +4506,8 @@ snapshots:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
uc.micro@2.1.0: {}
|
||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
@ -4205,10 +4532,16 @@ snapshots:
|
||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
utf7@1.0.2:
|
||||||
|
dependencies:
|
||||||
|
semver: 5.3.0
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
uuid@8.3.2: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
uuid@9.0.1: {}
|
||||||
|
|
||||||
varint@6.0.0: {}
|
varint@6.0.0: {}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
onlyBuiltDependencies:
|
onlyBuiltDependencies:
|
||||||
|
- '@prisma/client'
|
||||||
- better-sqlite3
|
- better-sqlite3
|
||||||
- esbuild
|
- esbuild
|
||||||
- sqlite3
|
- sqlite3
|
||||||
|
|
101
prisma/schema.prisma
Normal file
101
prisma/schema.prisma
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
||||||
|
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Form {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
description String?
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
categories Category[]
|
||||||
|
responses Response[]
|
||||||
|
|
||||||
|
@@map("forms") // Map to the 'forms' table
|
||||||
|
}
|
||||||
|
|
||||||
|
model Category {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
formId Int @map("form_id")
|
||||||
|
name String
|
||||||
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||||
|
fields Field[]
|
||||||
|
|
||||||
|
@@map("categories") // Map to the 'categories' table
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FieldType {
|
||||||
|
text
|
||||||
|
number
|
||||||
|
date
|
||||||
|
textarea
|
||||||
|
boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
model Field {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
categoryId Int @map("category_id")
|
||||||
|
label String
|
||||||
|
type FieldType // Using Prisma Enum based on CHECK constraint
|
||||||
|
description String?
|
||||||
|
sortOrder Int @map("sort_order")
|
||||||
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
||||||
|
responseValues ResponseValue[]
|
||||||
|
|
||||||
|
@@map("fields") // Map to the 'fields' table
|
||||||
|
}
|
||||||
|
|
||||||
|
model Response {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
formId Int @map("form_id")
|
||||||
|
submittedAt DateTime @default(now()) @map("submitted_at")
|
||||||
|
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||||
|
responseValues ResponseValue[]
|
||||||
|
|
||||||
|
@@map("responses") // Map to the 'responses' table
|
||||||
|
}
|
||||||
|
|
||||||
|
model ResponseValue {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
responseId Int @map("response_id")
|
||||||
|
fieldId Int @map("field_id")
|
||||||
|
value String?
|
||||||
|
response Response @relation(fields: [responseId], references: [id], onDelete: Cascade)
|
||||||
|
field Field @relation(fields: [fieldId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("response_values") // Map to the 'response_values' table
|
||||||
|
}
|
||||||
|
|
||||||
|
model MantisSummary {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
summaryDate DateTime @unique @db.Date @map("summary_date")
|
||||||
|
summaryText String @map("summary_text")
|
||||||
|
generatedAt DateTime @default(now()) @map("generated_at")
|
||||||
|
|
||||||
|
@@map("mantis_summaries") // Map to the 'mantis_summaries' table
|
||||||
|
}
|
||||||
|
|
||||||
|
model EmailSummary {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
summaryDate DateTime @unique @db.Date
|
||||||
|
summaryText String
|
||||||
|
generatedAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model Setting {
|
||||||
|
key String @id
|
||||||
|
value String
|
||||||
|
|
||||||
|
@@map("settings") // Map to the 'settings' table
|
||||||
|
}
|
|
@ -73,7 +73,7 @@ export default defineConfig((/* ctx */) => {
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||||
framework: {
|
framework: {
|
||||||
config: {
|
config: {
|
||||||
dark: "auto"
|
dark: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// iconSet: 'material-icons', // Quasar icon set
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
|
|
@ -1,110 +1,14 @@
|
||||||
import Database from 'better-sqlite3';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { join } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import fs from 'fs'; // Needed to check if db file exists
|
|
||||||
|
|
||||||
// Determine the database path relative to this file
|
// Instantiate Prisma Client
|
||||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
const prisma = new PrismaClient();
|
||||||
const dbPath = join(__dirname, 'forms.db');
|
|
||||||
|
|
||||||
let db = null;
|
// Export the Prisma Client instance for use in other modules
|
||||||
|
export default prisma;
|
||||||
|
|
||||||
export function initializeDatabase() {
|
// --- Old better-sqlite3 code removed ---
|
||||||
if (db) {
|
// No need for initializeDatabase, getDb, closeDatabase, etc.
|
||||||
return db;
|
// Prisma Client manages the connection pool.
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// --- Settings Functions removed ---
|
||||||
// Check if the directory exists, create if not (better-sqlite3 might need this)
|
// Settings can now be accessed via prisma.setting.findUnique, prisma.setting.upsert, etc.
|
||||||
const dbDir = join(__dirname);
|
|
||||||
if (!fs.existsSync(dbDir)) {
|
|
||||||
fs.mkdirSync(dbDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// better-sqlite3 constructor opens/creates the database file
|
|
||||||
db = new Database(dbPath, { verbose: console.log }); // Enable verbose logging
|
|
||||||
|
|
||||||
console.log('Connected to the SQLite database using better-sqlite3.');
|
|
||||||
|
|
||||||
// Ensure WAL mode is enabled for better concurrency
|
|
||||||
db.pragma('journal_mode = WAL');
|
|
||||||
|
|
||||||
// Create tables if they don't exist (run sequentially)
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS forms (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS categories (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
formId INTEGER NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
sortOrder INTEGER DEFAULT 0,
|
|
||||||
FOREIGN KEY (formId) REFERENCES forms (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS fields (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
categoryId INTEGER NOT NULL,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL CHECK(type IN ('text', 'number', 'date', 'textarea', 'boolean')),
|
|
||||||
description TEXT,
|
|
||||||
sortOrder INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (categoryId) REFERENCES categories(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS responses (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
formId INTEGER NOT NULL,
|
|
||||||
submittedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (formId) REFERENCES forms (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS response_values (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
responseId INTEGER NOT NULL,
|
|
||||||
fieldId INTEGER NOT NULL,
|
|
||||||
value TEXT,
|
|
||||||
FOREIGN KEY (responseId) REFERENCES responses (id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (fieldId) REFERENCES fields (id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('Database tables ensured.');
|
|
||||||
return db;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error initializing database with better-sqlite3:', err.message);
|
|
||||||
throw err; // Re-throw the error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDb() {
|
|
||||||
if (!db) {
|
|
||||||
// Try to initialize if not already done (e.g., during hot reload)
|
|
||||||
try {
|
|
||||||
initializeDatabase();
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error('Database not initialized and initialization failed.');
|
|
||||||
}
|
|
||||||
if (!db) { // Check again after trying to initialize
|
|
||||||
throw new Error('Database not initialized. Call initializeDatabase first.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: Add a function to close the database gracefully on server shutdown
|
|
||||||
export function closeDatabase() {
|
|
||||||
if (db) {
|
|
||||||
db.close();
|
|
||||||
db = null;
|
|
||||||
console.log('Database connection closed.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,9 +19,10 @@ import {
|
||||||
defineSsrRenderPreloadTag
|
defineSsrRenderPreloadTag
|
||||||
} from '#q-app/wrappers'
|
} from '#q-app/wrappers'
|
||||||
|
|
||||||
// Import database initialization and close function
|
import prisma from './database.js'; // Import the prisma client instance
|
||||||
import { initializeDatabase, closeDatabase } from './database.js';
|
|
||||||
import apiRoutes from './routes/api.js';
|
import apiRoutes from './routes/api.js';
|
||||||
|
import cron from 'node-cron';
|
||||||
|
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create your webserver and return its instance.
|
* Create your webserver and return its instance.
|
||||||
|
@ -35,12 +36,31 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
||||||
|
|
||||||
// Initialize the database (now synchronous)
|
// Initialize the database (now synchronous)
|
||||||
try {
|
try {
|
||||||
initializeDatabase();
|
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
||||||
console.log('Database initialized successfully.');
|
|
||||||
|
// Schedule the Mantis summary task after DB initialization
|
||||||
|
// Run daily at 1:00 AM server time (adjust as needed)
|
||||||
|
cron.schedule('0 1 * * *', async () => {
|
||||||
|
console.log('Running scheduled Mantis summary task...');
|
||||||
|
try {
|
||||||
|
await generateAndStoreMantisSummary();
|
||||||
|
console.log('Scheduled Mantis summary task completed.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error running scheduled Mantis summary task:', error);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
scheduled: true,
|
||||||
|
timezone: "Europe/London" // Example: Set to your server's timezone
|
||||||
|
});
|
||||||
|
console.log('Mantis summary cron job scheduled.');
|
||||||
|
|
||||||
|
// Optional: Run once immediately on server start if needed
|
||||||
|
generateAndStoreMantisSummary().catch(err => console.error('Initial Mantis summary failed:', err));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize database:', error);
|
console.error('Error during server setup:', error);
|
||||||
// Optionally handle the error more gracefully, e.g., prevent server start
|
// Optionally handle the error more gracefully, e.g., prevent server start
|
||||||
process.exit(1); // Exit if DB connection fails
|
process.exit(1); // Exit if setup fails
|
||||||
}
|
}
|
||||||
|
|
||||||
// attackers can use this header to detect apps running Express
|
// attackers can use this header to detect apps running Express
|
||||||
|
@ -91,9 +111,14 @@ export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
|
||||||
*
|
*
|
||||||
* Can be async: defineSsrClose(async ({ ... }) => { ... })
|
* Can be async: defineSsrClose(async ({ ... }) => { ... })
|
||||||
*/
|
*/
|
||||||
export const close = defineSsrClose(({ listenResult }) => {
|
export const close = defineSsrClose(async ({ listenResult }) => {
|
||||||
// Close the database connection when the server shuts down
|
// Close the database connection when the server shuts down
|
||||||
closeDatabase();
|
try {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
console.log('Prisma Client disconnected.');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error disconnecting Prisma Client:', e);
|
||||||
|
}
|
||||||
|
|
||||||
return listenResult.close()
|
return listenResult.close()
|
||||||
})
|
})
|
||||||
|
|
199
src-ssr/services/emailSummarizer.js
Normal file
199
src-ssr/services/emailSummarizer.js
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
import Imap from 'node-imap';
|
||||||
|
import { simpleParser } from 'mailparser';
|
||||||
|
import { GoogleGenAI } from '@google/genai';
|
||||||
|
import prisma from '../database.js';
|
||||||
|
|
||||||
|
// --- Environment Variables ---
|
||||||
|
const { GOOGLE_API_KEY } = process.env; // Added
|
||||||
|
|
||||||
|
// --- AI Setup ---
|
||||||
|
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||||
|
apiKey: GOOGLE_API_KEY,
|
||||||
|
}) : null; // Added
|
||||||
|
|
||||||
|
export async function fetchAndFormatEmails() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const imapConfig = {
|
||||||
|
user: process.env.OUTLOOK_EMAIL_ADDRESS,
|
||||||
|
password: process.env.OUTLOOK_APP_PASSWORD,
|
||||||
|
host: 'outlook.office365.com',
|
||||||
|
port: 993,
|
||||||
|
tls: true,
|
||||||
|
tlsOptions: { rejectUnauthorized: false } // Adjust as needed for your environment
|
||||||
|
};
|
||||||
|
|
||||||
|
const imap = new Imap(imapConfig);
|
||||||
|
const emailsJson = [];
|
||||||
|
|
||||||
|
function openInbox(cb) {
|
||||||
|
// Note: IMAP uses '/' as hierarchy separator, adjust if your server uses something else
|
||||||
|
imap.openBox('SLSNotifications/Reports/Backups', false, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
imap.once('ready', () => {
|
||||||
|
openInbox((err, box) => {
|
||||||
|
if (err) {
|
||||||
|
imap.end();
|
||||||
|
return reject(new Error(`Error opening mailbox: ${err.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const searchCriteria = [['SINCE', yesterday.toISOString().split('T')[0]]]; // Search since midnight yesterday
|
||||||
|
const fetchOptions = { bodies: ['HEADER.FIELDS (SUBJECT DATE)', 'TEXT'], struct: true };
|
||||||
|
|
||||||
|
imap.search(searchCriteria, (searchErr, results) => {
|
||||||
|
if (searchErr) {
|
||||||
|
imap.end();
|
||||||
|
return reject(new Error(`Error searching emails: ${searchErr.message}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
console.log('No emails found from the last 24 hours.');
|
||||||
|
imap.end();
|
||||||
|
return resolve([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const f = imap.fetch(results, fetchOptions);
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
f.on('message', (msg, seqno) => {
|
||||||
|
let header = '';
|
||||||
|
let body = '';
|
||||||
|
|
||||||
|
msg.on('body', (stream, info) => {
|
||||||
|
let buffer = '';
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
buffer += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
stream.once('end', () => {
|
||||||
|
if (info.which === 'TEXT') {
|
||||||
|
body = buffer;
|
||||||
|
} else {
|
||||||
|
// Assuming HEADER.FIELDS (SUBJECT DATE) comes as one chunk
|
||||||
|
header = buffer;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
msg.once('attributes', (attrs) => {
|
||||||
|
// Attributes might contain date if not fetched via header
|
||||||
|
});
|
||||||
|
|
||||||
|
msg.once('end', async () => {
|
||||||
|
try {
|
||||||
|
// Use mailparser to handle potential encoding issues and structure
|
||||||
|
const mail = await simpleParser(`Subject: ${header.match(/Subject: (.*)/i)?.[1] || ''}\nDate: ${header.match(/Date: (.*)/i)?.[1] || ''}\n\n${body}`);
|
||||||
|
emailsJson.push({
|
||||||
|
title: mail.subject || 'No Subject',
|
||||||
|
time: mail.date ? mail.date.toISOString() : 'No Date',
|
||||||
|
body: mail.text || mail.html || 'No Body Content' // Prefer text, fallback to html, then empty
|
||||||
|
});
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.error(`Error parsing email seqno ${seqno}:`, parseErr);
|
||||||
|
// Decide if you want to reject or just skip this email
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount++;
|
||||||
|
if (processedCount === results.length) {
|
||||||
|
// This check might be slightly inaccurate if errors occur,
|
||||||
|
// but it's a common pattern. Consider refining with promises.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once('error', (fetchErr) => {
|
||||||
|
console.error('Fetch error: ' + fetchErr);
|
||||||
|
// Don't reject here immediately, might still get some emails
|
||||||
|
});
|
||||||
|
|
||||||
|
f.once('end', () => {
|
||||||
|
console.log('Done fetching all messages!');
|
||||||
|
imap.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.once('error', (err) => {
|
||||||
|
reject(new Error(`IMAP Connection Error: ${err.message}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.once('end', () => {
|
||||||
|
console.log('IMAP Connection ended.');
|
||||||
|
resolve(emailsJson); // Resolve with the collected emails
|
||||||
|
});
|
||||||
|
|
||||||
|
imap.connect();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Email Summary Logic (New Function) ---
|
||||||
|
export async function generateAndStoreEmailSummary() {
|
||||||
|
console.log('Attempting to generate and store Email summary...');
|
||||||
|
if (!ai) {
|
||||||
|
console.error('Google AI API key not configured. Skipping email summary generation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the prompt from the database settings using Prisma
|
||||||
|
const setting = await prisma.setting.findUnique({
|
||||||
|
where: { key: 'emailPrompt' }, // Use 'emailPrompt' as the key
|
||||||
|
select: { value: true }
|
||||||
|
});
|
||||||
|
const promptTemplate = setting?.value;
|
||||||
|
|
||||||
|
if (!promptTemplate) {
|
||||||
|
console.error('Email prompt not found in database settings (key: emailPrompt). Skipping summary generation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emails = await fetchAndFormatEmails();
|
||||||
|
|
||||||
|
let summaryText;
|
||||||
|
if (emails.length === 0) {
|
||||||
|
summaryText = "No relevant emails found in the last 24 hours.";
|
||||||
|
console.log('No recent emails found for summary.');
|
||||||
|
} else {
|
||||||
|
console.log(`Found ${emails.length} recent emails. Generating summary...`);
|
||||||
|
// Replace placeholder in the prompt template
|
||||||
|
// Ensure your prompt template uses $EMAIL_DATA
|
||||||
|
let prompt = promptTemplate.replaceAll("$EMAIL_DATA", JSON.stringify(emails, null, 2));
|
||||||
|
|
||||||
|
// Call the AI model (adjust model name and config as needed)
|
||||||
|
const response = await ai.models.generateContent({
|
||||||
|
"model": "gemini-2.5-preview-04-17",
|
||||||
|
"contents": prompt,
|
||||||
|
config: {
|
||||||
|
temperature: 0 // Adjust temperature as needed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
summaryText = response.text;
|
||||||
|
console.log('Email summary generated successfully by AI.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the summary in the database using Prisma upsert
|
||||||
|
const today = new Date();
|
||||||
|
today.setUTCHours(0, 0, 0, 0); // Use UTC start of day for consistency
|
||||||
|
|
||||||
|
await prisma.emailSummary.upsert({
|
||||||
|
where: { summaryDate: today },
|
||||||
|
update: {
|
||||||
|
summaryText: summaryText,
|
||||||
|
// generatedAt is updated automatically by @default(now())
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
summaryDate: today,
|
||||||
|
summaryText: summaryText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Email summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Email summary generation/storage:", error);
|
||||||
|
// Re-throw or handle as appropriate for your application
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
171
src-ssr/services/mantisSummarizer.js
Normal file
171
src-ssr/services/mantisSummarizer.js
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { GoogleGenAI } from '@google/genai';
|
||||||
|
import prisma from '../database.js'; // Import Prisma client
|
||||||
|
|
||||||
|
// --- Environment Variables ---
|
||||||
|
const {
|
||||||
|
MANTIS_API_KEY,
|
||||||
|
MANTIS_API_ENDPOINT,
|
||||||
|
GOOGLE_API_KEY
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
// --- Mantis Summarizer Setup ---
|
||||||
|
const ai = GOOGLE_API_KEY ? new GoogleGenAI({
|
||||||
|
apiKey: GOOGLE_API_KEY,
|
||||||
|
}) : null;
|
||||||
|
|
||||||
|
const usernameMap = {
|
||||||
|
'credmore': 'Cameron Redmore',
|
||||||
|
'dgibson': 'Dane Gibson',
|
||||||
|
'egzibovskis': 'Ed Gzibovskis',
|
||||||
|
'ascotney': 'Amanda Scotney',
|
||||||
|
'gclough': 'Garry Clough',
|
||||||
|
'slee': 'Sarah Lee',
|
||||||
|
'dwalker': 'Dave Walker',
|
||||||
|
'askaith': 'Amy Skaith',
|
||||||
|
'dpotter': 'Danny Potter',
|
||||||
|
'msmart': 'Michael Smart',
|
||||||
|
// Add other usernames as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getMantisTickets() {
|
||||||
|
if (!MANTIS_API_ENDPOINT || !MANTIS_API_KEY) {
|
||||||
|
throw new Error("Mantis API endpoint or key not configured in environment variables.");
|
||||||
|
}
|
||||||
|
const url = `${MANTIS_API_ENDPOINT}/issues?project_id=1&page_size=50&select=id,summary,description,created_at,updated_at,reporter,notes`;
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `${MANTIS_API_KEY}`,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url, { headers });
|
||||||
|
|
||||||
|
const tickets = response.data.issues.filter((ticket) => {
|
||||||
|
const ticketDate = new Date(ticket.updated_at);
|
||||||
|
const thresholdDate = new Date();
|
||||||
|
const currentDay = thresholdDate.getDay(); // Sunday = 0, Monday = 1, ...
|
||||||
|
|
||||||
|
// Go back 4 days if Monday (to include Fri, Sat, Sun), otherwise 2 days
|
||||||
|
const daysToSubtract = currentDay === 1 ? 4 : 2;
|
||||||
|
thresholdDate.setDate(thresholdDate.getDate() - daysToSubtract);
|
||||||
|
thresholdDate.setHours(0, 0, 0, 0); // Start of the day
|
||||||
|
|
||||||
|
return ticketDate >= thresholdDate;
|
||||||
|
}).map((ticket) => {
|
||||||
|
return {
|
||||||
|
id: ticket.id,
|
||||||
|
summary: ticket.summary,
|
||||||
|
description: ticket.description,
|
||||||
|
created_at: ticket.created_at,
|
||||||
|
updated_at: ticket.updated_at,
|
||||||
|
reporter: usernameMap[ticket.reporter?.username] || ticket.reporter?.name || 'Unknown Reporter', // Safer access
|
||||||
|
notes: (ticket.notes ? ticket.notes.filter((note) => {
|
||||||
|
const noteDate = new Date(note.created_at);
|
||||||
|
const thresholdDate = new Date();
|
||||||
|
const currentDay = thresholdDate.getDay();
|
||||||
|
const daysToSubtract = currentDay === 1 ? 4 : 2;
|
||||||
|
thresholdDate.setDate(thresholdDate.getDate() - daysToSubtract);
|
||||||
|
thresholdDate.setHours(0, 0, 0, 0); // Start of the day
|
||||||
|
return noteDate >= thresholdDate;
|
||||||
|
}) : []).map((note) => {
|
||||||
|
const reporter = usernameMap[note.reporter?.username] || note.reporter?.name || 'Unknown Reporter'; // Safer access
|
||||||
|
return {
|
||||||
|
reporter,
|
||||||
|
created_at: note.created_at,
|
||||||
|
text: note.text,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return tickets;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching Mantis tickets:", error.message);
|
||||||
|
// Check if it's an Axios error and provide more details
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error("Axios error details:", error.response?.status, error.response?.data);
|
||||||
|
throw new Error(`Failed to fetch Mantis tickets: ${error.response?.statusText || error.message}`);
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to fetch Mantis tickets: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mantis Summary Logic (Exported) --- //
|
||||||
|
|
||||||
|
export async function generateAndStoreMantisSummary() {
|
||||||
|
console.log('Attempting to generate and store Mantis summary...');
|
||||||
|
if (!ai) {
|
||||||
|
console.error('Google AI API key not configured. Skipping summary generation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the prompt from the database settings using Prisma
|
||||||
|
const setting = await prisma.setting.findUnique({
|
||||||
|
where: { key: 'mantisPrompt' },
|
||||||
|
select: { value: true }
|
||||||
|
});
|
||||||
|
const promptTemplate = setting?.value;
|
||||||
|
|
||||||
|
if (!promptTemplate) {
|
||||||
|
console.error('Mantis prompt not found in database settings (key: mantisPrompt). Skipping summary generation.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tickets = await getMantisTickets();
|
||||||
|
|
||||||
|
let summaryText;
|
||||||
|
if (tickets.length === 0) {
|
||||||
|
summaryText = "No Mantis tickets updated recently.";
|
||||||
|
console.log('No recent Mantis tickets found.');
|
||||||
|
} else {
|
||||||
|
console.log(`Found ${tickets.length} recent Mantis tickets. Generating summary...`);
|
||||||
|
let prompt = promptTemplate.replaceAll("$DATE", new Date().toISOString().split('T')[0]);
|
||||||
|
prompt = prompt.replaceAll("$MANTIS_TICKETS", JSON.stringify(tickets, null, 2));
|
||||||
|
|
||||||
|
const response = await ai.models.generateContent({
|
||||||
|
"model": "gemini-2.5-flash-preview-04-17",
|
||||||
|
"contents": prompt,
|
||||||
|
config: {
|
||||||
|
temperature: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
summaryText = response.text;
|
||||||
|
console.log('Mantis summary generated successfully by AI.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the summary in the database using Prisma upsert
|
||||||
|
const today = new Date();
|
||||||
|
today.setUTCHours(0, 0, 0, 0); // Use UTC start of day for consistency
|
||||||
|
|
||||||
|
await prisma.mantisSummary.upsert({
|
||||||
|
where: { summaryDate: today },
|
||||||
|
update: {
|
||||||
|
summaryText: summaryText,
|
||||||
|
// generatedAt is updated automatically by @default(now())
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
summaryDate: today,
|
||||||
|
summaryText: summaryText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(`Mantis summary for ${today.toISOString().split('T')[0]} stored/updated in the database.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during Mantis summary generation/storage:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateTodaysSummary() {
|
||||||
|
console.log('Triggering Mantis summary generation via generateTodaysSummary...');
|
||||||
|
try {
|
||||||
|
await generateAndStoreMantisSummary();
|
||||||
|
return { success: true, message: 'Summary generation process initiated.' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error occurred within generateTodaysSummary while calling generateAndStoreMantisSummary:', error);
|
||||||
|
throw new Error('Failed to initiate Mantis summary generation.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,53 @@
|
||||||
<q-item-label caption>Create a new form</q-item-label>
|
<q-item-label caption>Create a new form</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
:to="{ name: 'mantisSummaries' }"
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="summarize" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Mantis Summaries</q-item-label>
|
||||||
|
<q-item-label caption>View daily summaries</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
:to="{ name: 'emailSummaries' }"
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="email" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Email Summaries</q-item-label>
|
||||||
|
<q-item-label caption>View email summaries</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
to="/settings" exact
|
||||||
|
>
|
||||||
|
<q-item-section
|
||||||
|
avatar
|
||||||
|
>
|
||||||
|
<q-icon name="settings" />
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>Settings</q-item-label>
|
||||||
|
<q-item-label caption>Manage application settings</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
|
|
||||||
|
|
201
src/pages/EmailSummariesPage.vue
Normal file
201
src/pages/EmailSummariesPage.vue
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section class="row items-center justify-between">
|
||||||
|
<div class="text-h6">Email Summaries</div>
|
||||||
|
<q-btn
|
||||||
|
label="Generate Email Summary"
|
||||||
|
color="primary"
|
||||||
|
@click="generateSummary"
|
||||||
|
:loading="generating"
|
||||||
|
:disable="generating"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section v-if="generationError">
|
||||||
|
<q-banner inline-actions class="text-white bg-red">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="error" />
|
||||||
|
</template>
|
||||||
|
{{ generationError }}
|
||||||
|
</q-banner>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="loading">
|
||||||
|
<q-spinner-dots size="40px" color="primary" />
|
||||||
|
<span class="q-ml-md">Loading summaries...</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="error && !generationError">
|
||||||
|
<q-banner inline-actions class="text-white bg-red">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="error" />
|
||||||
|
</template>
|
||||||
|
{{ error }}
|
||||||
|
</q-banner>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-list separator v-if="!loading && !error && summaries.length > 0">
|
||||||
|
<q-item v-for="summary in summaries" :key="summary.id">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-weight-bold">{{ formatDate(summary.summaryDate) }}</q-item-label>
|
||||||
|
<q-item-label caption>Generated: {{ formatDateTime(summary.generatedAt) }}</q-item-label>
|
||||||
|
<q-item-label class="q-mt-sm markdown-content" v-html="parseMarkdown(summary.summaryText)"></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<q-card-section v-if="totalPages > 1" class="flex flex-center q-mt-md">
|
||||||
|
<q-pagination
|
||||||
|
v-model="currentPage"
|
||||||
|
:max="totalPages"
|
||||||
|
@update:model-value="fetchSummaries"
|
||||||
|
direction-links
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
active-color="primary"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="!loading && !error && summaries.length === 0">
|
||||||
|
<div class="text-center text-grey">No summaries found.</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
||||||
|
import axios from 'axios';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
const $q = useQuasar(); // Initialize Quasar plugin usage
|
||||||
|
const summaries = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
const generating = ref(false); // State for generation button
|
||||||
|
const generationError = ref(null); // State for generation error
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const itemsPerPage = ref(10); // Or your desired page size
|
||||||
|
const totalItems = ref(0);
|
||||||
|
|
||||||
|
// Create a custom renderer
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
const linkRenderer = renderer.link;
|
||||||
|
renderer.link = (href, title, text) => {
|
||||||
|
const html = linkRenderer.call(renderer, href, title, text);
|
||||||
|
// Add target="_blank" to the link
|
||||||
|
return html.replace(/^<a /, '<a target="_blank" rel="noopener noreferrer" ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSummaries = async (page = 1) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// *** CHANGED API ENDPOINT ***
|
||||||
|
const response = await axios.get(`/api/email-summaries`, {
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
limit: itemsPerPage.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
summaries.value = response.data.summaries;
|
||||||
|
totalItems.value = response.data.total;
|
||||||
|
currentPage.value = page;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching Email summaries:', err);
|
||||||
|
error.value = err.response?.data?.error || 'Failed to load summaries. Please try again later.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSummary = async () => {
|
||||||
|
generating.value = true;
|
||||||
|
generationError.value = null;
|
||||||
|
error.value = null; // Clear previous loading errors
|
||||||
|
try {
|
||||||
|
// *** CHANGED API ENDPOINT ***
|
||||||
|
await axios.post('/api/email-summaries/generate');
|
||||||
|
$q.notify({
|
||||||
|
color: 'positive',
|
||||||
|
icon: 'check_circle',
|
||||||
|
// *** CHANGED MESSAGE ***
|
||||||
|
message: 'Email summary generation started successfully. It may take a few moments to appear.',
|
||||||
|
});
|
||||||
|
// Optionally, refresh the list after a short delay or immediately
|
||||||
|
// Consider that generation might be async on the backend
|
||||||
|
setTimeout(() => fetchSummaries(1), 3000); // Refresh after 3 seconds
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating Email summary:', err);
|
||||||
|
// *** CHANGED MESSAGE ***
|
||||||
|
generationError.value = err.response?.data?.error || 'Failed to start email summary generation.';
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'error',
|
||||||
|
message: generationError.value,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
generating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
// Assuming dateString is YYYY-MM-DD
|
||||||
|
return date.formatDate(dateString + 'T00:00:00', 'DD MMMM YYYY');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateTimeString) => {
|
||||||
|
return date.formatDate(dateTimeString, 'DD MMMM YYYY HH:mm');
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMarkdown = (markdownText) => {
|
||||||
|
if (!markdownText) return '';
|
||||||
|
// Use the custom renderer with marked
|
||||||
|
return marked(markdownText, { renderer });
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSummaries(currentPage.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-content :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(th),
|
||||||
|
.markdown-content :deep(td) {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(th) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(a) {
|
||||||
|
color: var(--q-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(a:hover) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add any specific styles if needed */
|
||||||
|
</style>
|
197
src/pages/MantisSummariesPage.vue
Normal file
197
src/pages/MantisSummariesPage.vue
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section class="row items-center justify-between">
|
||||||
|
<div class="text-h6">Mantis Summaries</div>
|
||||||
|
<q-btn
|
||||||
|
label="Generate Today's Summary"
|
||||||
|
color="primary"
|
||||||
|
@click="generateSummary"
|
||||||
|
:loading="generating"
|
||||||
|
:disable="generating"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-card-section v-if="generationError">
|
||||||
|
<q-banner inline-actions class="text-white bg-red">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="error" />
|
||||||
|
</template>
|
||||||
|
{{ generationError }}
|
||||||
|
</q-banner>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="loading">
|
||||||
|
<q-spinner-dots size="40px" color="primary" />
|
||||||
|
<span class="q-ml-md">Loading summaries...</span>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="error && !generationError">
|
||||||
|
<q-banner inline-actions class="text-white bg-red">
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="error" />
|
||||||
|
</template>
|
||||||
|
{{ error }}
|
||||||
|
</q-banner>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-list separator v-if="!loading && !error && summaries.length > 0">
|
||||||
|
<q-item v-for="summary in summaries" :key="summary.id">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label class="text-weight-bold">{{ formatDate(summary.summaryDate) }}</q-item-label>
|
||||||
|
<q-item-label caption>Generated: {{ formatDateTime(summary.generatedAt) }}</q-item-label>
|
||||||
|
<q-item-label class="q-mt-sm markdown-content" v-html="parseMarkdown(summary.summaryText)"></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<q-card-section v-if="totalPages > 1" class="flex flex-center q-mt-md">
|
||||||
|
<q-pagination
|
||||||
|
v-model="currentPage"
|
||||||
|
:max="totalPages"
|
||||||
|
@update:model-value="fetchSummaries"
|
||||||
|
direction-links
|
||||||
|
flat
|
||||||
|
color="primary"
|
||||||
|
active-color="primary"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section v-if="!loading && !error && summaries.length === 0">
|
||||||
|
<div class="text-center text-grey">No summaries found.</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { date, useQuasar } from 'quasar'; // Import useQuasar
|
||||||
|
import axios from 'axios';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
const $q = useQuasar(); // Initialize Quasar plugin usage
|
||||||
|
const summaries = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const error = ref(null);
|
||||||
|
const generating = ref(false); // State for generation button
|
||||||
|
const generationError = ref(null); // State for generation error
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const itemsPerPage = ref(10); // Or your desired page size
|
||||||
|
const totalItems = ref(0);
|
||||||
|
|
||||||
|
// Create a custom renderer
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
const linkRenderer = renderer.link;
|
||||||
|
renderer.link = (href, title, text) => {
|
||||||
|
const html = linkRenderer.call(renderer, href, title, text);
|
||||||
|
// Add target="_blank" to the link
|
||||||
|
return html.replace(/^<a /, '<a target="_blank" rel="noopener noreferrer" ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSummaries = async (page = 1) => {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/mantis-summaries`, {
|
||||||
|
params: {
|
||||||
|
page: page,
|
||||||
|
limit: itemsPerPage.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
summaries.value = response.data.summaries;
|
||||||
|
totalItems.value = response.data.total;
|
||||||
|
currentPage.value = page;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching Mantis summaries:', err);
|
||||||
|
error.value = err.response?.data?.error || 'Failed to load summaries. Please try again later.';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateSummary = async () => {
|
||||||
|
generating.value = true;
|
||||||
|
generationError.value = null;
|
||||||
|
error.value = null; // Clear previous loading errors
|
||||||
|
try {
|
||||||
|
await axios.post('/api/mantis-summaries/generate');
|
||||||
|
$q.notify({
|
||||||
|
color: 'positive',
|
||||||
|
icon: 'check_circle',
|
||||||
|
message: 'Summary generation started successfully. It may take a few moments to appear.',
|
||||||
|
});
|
||||||
|
// Optionally, refresh the list after a short delay or immediately
|
||||||
|
// Consider that generation might be async on the backend
|
||||||
|
setTimeout(() => fetchSummaries(1), 3000); // Refresh after 3 seconds
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating Mantis summary:', err);
|
||||||
|
generationError.value = err.response?.data?.error || 'Failed to start summary generation.';
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
icon: 'error',
|
||||||
|
message: generationError.value,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
generating.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
// Assuming dateString is YYYY-MM-DD
|
||||||
|
return date.formatDate(dateString + 'T00:00:00', 'DD MMMM YYYY');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (dateTimeString) => {
|
||||||
|
return date.formatDate(dateTimeString, 'DD MMMM YYYY HH:mm');
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMarkdown = (markdownText) => {
|
||||||
|
if (!markdownText) return '';
|
||||||
|
// Use the custom renderer with marked
|
||||||
|
return marked(markdownText, { renderer });
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(totalItems.value / itemsPerPage.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSummaries(currentPage.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-content :deep(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(th),
|
||||||
|
.markdown-content :deep(td) {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(th) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(a) {
|
||||||
|
color: var(--q-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(a:hover) {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add any specific styles if needed */
|
||||||
|
</style>
|
163
src/pages/SettingsPage.vue
Normal file
163
src/pages/SettingsPage.vue
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<div class="q-gutter-md" style="max-width: 800px; margin: auto;">
|
||||||
|
<h5 class="q-mt-none q-mb-md">Settings</h5>
|
||||||
|
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Mantis Summary Prompt</div>
|
||||||
|
<div class="text-caption text-grey q-mb-sm">
|
||||||
|
Edit the prompt used to generate Mantis summaries. Use $DATE and $MANTIS_TICKETS as placeholders.
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
|
v-model="mantisPrompt"
|
||||||
|
type="textarea"
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
label="Mantis Prompt"
|
||||||
|
:loading="loadingPrompt"
|
||||||
|
:disable="savingPrompt"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
label="Save Prompt"
|
||||||
|
color="primary"
|
||||||
|
@click="saveMantisPrompt"
|
||||||
|
:loading="savingPrompt"
|
||||||
|
:disable="!mantisPrompt || loadingPrompt"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card flat bordered>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Email Summary Prompt</div>
|
||||||
|
<div class="text-caption text-grey q-mb-sm">
|
||||||
|
Edit the prompt used to generate Email summaries. Use $EMAIL_DATA as a placeholder for the JSON email array.
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
|
v-model="emailPrompt"
|
||||||
|
type="textarea"
|
||||||
|
filled
|
||||||
|
autogrow
|
||||||
|
label="Email Prompt"
|
||||||
|
:loading="loadingEmailPrompt"
|
||||||
|
:disable="savingEmailPrompt"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
label="Save Prompt"
|
||||||
|
color="primary"
|
||||||
|
@click="saveEmailPrompt"
|
||||||
|
:loading="savingEmailPrompt"
|
||||||
|
:disable="!emailPrompt || loadingEmailPrompt"
|
||||||
|
/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const mantisPrompt = ref('');
|
||||||
|
const loadingPrompt = ref(false);
|
||||||
|
const savingPrompt = ref(false);
|
||||||
|
|
||||||
|
const fetchMantisPrompt = async () => {
|
||||||
|
loadingPrompt.value = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/settings/mantisPrompt');
|
||||||
|
mantisPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Mantis prompt:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to load Mantis prompt setting.',
|
||||||
|
icon: 'report_problem'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loadingPrompt.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveMantisPrompt = async () => {
|
||||||
|
savingPrompt.value = true;
|
||||||
|
try {
|
||||||
|
await axios.put('/api/settings/mantisPrompt', { value: mantisPrompt.value });
|
||||||
|
$q.notify({
|
||||||
|
color: 'positive',
|
||||||
|
message: 'Mantis prompt updated successfully.',
|
||||||
|
icon: 'check_circle'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving Mantis prompt:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to save Mantis prompt setting.',
|
||||||
|
icon: 'report_problem'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
savingPrompt.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const emailPrompt = ref('');
|
||||||
|
const loadingEmailPrompt = ref(false);
|
||||||
|
const savingEmailPrompt = ref(false);
|
||||||
|
|
||||||
|
const fetchEmailPrompt = async () => {
|
||||||
|
loadingEmailPrompt.value = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/settings/emailPrompt');
|
||||||
|
emailPrompt.value = response.data.value || ''; // Handle case where setting might not exist yet
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Email prompt:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to load Email prompt setting.',
|
||||||
|
icon: 'report_problem'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loadingEmailPrompt.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEmailPrompt = async () => {
|
||||||
|
savingEmailPrompt.value = true;
|
||||||
|
try {
|
||||||
|
await axios.put('/api/settings/emailPrompt', { value: emailPrompt.value });
|
||||||
|
$q.notify({
|
||||||
|
color: 'positive',
|
||||||
|
message: 'Email prompt updated successfully.',
|
||||||
|
icon: 'check_circle'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving Email prompt:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to save Email prompt setting.',
|
||||||
|
icon: 'report_problem'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
savingEmailPrompt.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchMantisPrompt();
|
||||||
|
fetchEmailPrompt();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Add any specific styles if needed */
|
||||||
|
</style>
|
|
@ -8,7 +8,10 @@ const routes = [
|
||||||
{ path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue') },
|
{ path: 'forms/new', name: 'formCreate', component: () => import('pages/FormCreatePage.vue') },
|
||||||
{ path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true },
|
{ path: 'forms/:id/edit', name: 'formEdit', component: () => import('pages/FormEditPage.vue'), props: true },
|
||||||
{ path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true },
|
{ path: 'forms/:id/fill', name: 'formFill', component: () => import('pages/FormFillPage.vue'), props: true },
|
||||||
{ path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true }
|
{ path: 'forms/:id/responses', name: 'formResponses', component: () => import('pages/FormResponsesPage.vue'), props: true },
|
||||||
|
{ path: 'mantis-summaries', name: 'mantisSummaries', component: () => import('pages/MantisSummariesPage.vue') },
|
||||||
|
{ path: 'email-summaries', name: 'emailSummaries', component: () => import('pages/EmailSummariesPage.vue') },
|
||||||
|
{ path: 'settings', name: 'settings', component: () => import('pages/SettingsPage.vue') }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue