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
|
||||
|
||||
# local .env files
|
||||
.env
|
||||
.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,
|
||||
"scripts": {
|
||||
"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",
|
||||
"postinstall": "quasar prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^0.9.0",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"@quasar/extras": "^1.16.4",
|
||||
"axios": "^1.8.4",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"date-fns": "^4.1.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",
|
||||
"pdfmake": "^0.2.18",
|
||||
"quasar": "^2.16.0",
|
||||
|
@ -28,7 +33,8 @@
|
|||
"devDependencies": {
|
||||
"@quasar/app-vite": "^2.1.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"postcss": "^8.4.14"
|
||||
"postcss": "^8.4.14",
|
||||
"prisma": "^6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
||||
|
|
333
pnpm-lock.yaml
generated
333
pnpm-lock.yaml
generated
|
@ -11,6 +11,9 @@ importers:
|
|||
'@google/genai':
|
||||
specifier: ^0.9.0
|
||||
version: 0.9.0
|
||||
'@prisma/client':
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0(prisma@6.6.0)
|
||||
'@quasar/extras':
|
||||
specifier: ^1.16.4
|
||||
version: 1.16.17
|
||||
|
@ -26,6 +29,18 @@ importers:
|
|||
dotenv:
|
||||
specifier: ^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:
|
||||
specifier: ^0.17.0
|
||||
version: 0.17.0
|
||||
|
@ -51,6 +66,9 @@ importers:
|
|||
postcss:
|
||||
specifier: ^8.4.14
|
||||
version: 8.5.3
|
||||
prisma:
|
||||
specifier: ^6.6.0
|
||||
version: 6.6.0
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -273,6 +291,36 @@ packages:
|
|||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-MvCfJrCbxUYvoGaK5jPq0h0hjO8mbxYOWngf+dIKrxhwb+1h5ERh6aVYEUuCtMIwTMEVfPkCez4DIfZIoReuDw==}
|
||||
engines: {node: ^30 || ^28 || ^26 || ^24 || ^22 || ^20 || ^18, npm: '>= 6.14.12', yarn: '>= 1.17.3'}
|
||||
|
@ -421,6 +469,9 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||
|
||||
|
@ -842,6 +893,10 @@ packages:
|
|||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -888,6 +943,19 @@ packages:
|
|||
dfa@1.2.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||
|
||||
|
@ -937,6 +1005,10 @@ packages:
|
|||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
|
||||
|
@ -960,6 +1032,11 @@ packages:
|
|||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild-register@3.6.0:
|
||||
resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.12 <1'
|
||||
|
||||
esbuild@0.25.3:
|
||||
resolution: {integrity: sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -1149,11 +1226,22 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
html-minifier-terser@7.2.0:
|
||||
resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
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:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -1304,9 +1392,24 @@ packages:
|
|||
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
|
@ -1323,6 +1426,17 @@ packages:
|
|||
magic-string@0.30.17:
|
||||
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:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
@ -1417,6 +1531,10 @@ packages:
|
|||
resolution: {integrity: sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-cron@3.0.3:
|
||||
resolution: {integrity: sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
@ -1430,9 +1548,17 @@ packages:
|
|||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
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:
|
||||
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:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -1493,6 +1619,9 @@ packages:
|
|||
param-case@3.0.4:
|
||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||
|
||||
parseley@0.12.1:
|
||||
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
@ -1521,6 +1650,9 @@ packages:
|
|||
resolution: {integrity: sha512-Fe+GnMS8EVZu5rci/CDaQ+xmUoHvx8P+rvIlrwSYM6A5c7Aik8G6lpJbddhjBE2jXGjv6WcUCFCB06uZbjxkMw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
|
@ -1550,6 +1682,16 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
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:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
|
@ -1567,6 +1709,10 @@ packages:
|
|||
pump@3.0.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
@ -1796,10 +1942,17 @@ packages:
|
|||
sax@1.4.1:
|
||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||
|
||||
selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
|
||||
selfsigned@2.4.1:
|
||||
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
semver@5.3.0:
|
||||
resolution: {integrity: sha512-mfmm3/H9+67MCVix1h+IXTpDwL6710LyHuk7+cWC9T1mE0qz4iHhh6r4hU2wrIT9iTsAAC2XQRvfblL028cpLw==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.1:
|
||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -1961,6 +2114,10 @@ packages:
|
|||
resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tlds@1.255.0:
|
||||
resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==}
|
||||
hasBin: true
|
||||
|
||||
tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
|
@ -2002,6 +2159,9 @@ packages:
|
|||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
|
||||
|
@ -2028,6 +2188,9 @@ packages:
|
|||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
utf7@1.0.2:
|
||||
resolution: {integrity: sha512-qQrPtYLLLl12NF4DrM9CvfkxkYI97xOb5dsnGZHE3teFr0tWiEZ9UdgMPczv24vl708cYMpe6mGXGHrotIp3Bw==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
|
@ -2035,6 +2198,10 @@ packages:
|
|||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
hasBin: true
|
||||
|
@ -2344,6 +2511,38 @@ snapshots:
|
|||
'@pkgjs/parseargs@0.11.0':
|
||||
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)':
|
||||
dependencies:
|
||||
'@quasar/render-ssr-error': 1.0.3
|
||||
|
@ -2479,6 +2678,11 @@ snapshots:
|
|||
'@rollup/rollup-win32-x64-msvc@4.40.0':
|
||||
optional: true
|
||||
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
@ -2954,6 +3158,8 @@ snapshots:
|
|||
|
||||
deep-extend@0.6.0: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
default-browser-id@5.0.0: {}
|
||||
|
||||
default-browser@5.2.1:
|
||||
|
@ -2991,6 +3197,24 @@ snapshots:
|
|||
|
||||
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:
|
||||
dependencies:
|
||||
no-case: 3.0.4
|
||||
|
@ -3034,6 +3258,8 @@ snapshots:
|
|||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
encoding-japanese@2.2.0: {}
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
@ -3055,6 +3281,13 @@ snapshots:
|
|||
has-tostringtag: 1.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:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.3
|
||||
|
@ -3313,6 +3546,8 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
html-minifier-terser@7.2.0:
|
||||
dependencies:
|
||||
camel-case: 4.1.2
|
||||
|
@ -3323,6 +3558,21 @@ snapshots:
|
|||
relateurl: 0.2.7
|
||||
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:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
|
@ -3473,11 +3723,28 @@ snapshots:
|
|||
dependencies:
|
||||
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:
|
||||
dependencies:
|
||||
base64-js: 0.0.8
|
||||
unicode-trie: 2.0.0
|
||||
|
||||
linkify-it@5.0.0:
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-symbols@4.1.0:
|
||||
|
@ -3495,6 +3762,27 @@ snapshots:
|
|||
dependencies:
|
||||
'@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: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
|
@ -3561,14 +3849,25 @@ snapshots:
|
|||
dependencies:
|
||||
semver: 7.7.1
|
||||
|
||||
node-cron@3.0.3:
|
||||
dependencies:
|
||||
uuid: 8.3.2
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
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: {}
|
||||
|
||||
nodemailer@6.9.16: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-range@0.1.2: {}
|
||||
|
@ -3632,6 +3931,11 @@ snapshots:
|
|||
dot-case: 3.0.4
|
||||
tslib: 2.8.1
|
||||
|
||||
parseley@0.12.1:
|
||||
dependencies:
|
||||
leac: 0.6.0
|
||||
peberminta: 0.9.0
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
pascal-case@3.1.2:
|
||||
|
@ -3665,6 +3969,8 @@ snapshots:
|
|||
iconv-lite: 0.6.3
|
||||
xmldoc: 1.3.0
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
@ -3702,6 +4008,15 @@ snapshots:
|
|||
tar-fs: 2.1.2
|
||||
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@0.11.10: {}
|
||||
|
@ -3718,6 +4033,8 @@ snapshots:
|
|||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
qs@6.13.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
@ -3941,11 +4258,17 @@ snapshots:
|
|||
|
||||
sax@1.4.1: {}
|
||||
|
||||
selderee@0.11.0:
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
|
||||
selfsigned@2.4.1:
|
||||
dependencies:
|
||||
'@types/node-forge': 1.3.11
|
||||
node-forge: 1.3.1
|
||||
|
||||
semver@5.3.0: {}
|
||||
|
||||
semver@7.7.1: {}
|
||||
|
||||
send@0.19.0:
|
||||
|
@ -4152,6 +4475,8 @@ snapshots:
|
|||
fdir: 6.4.4(picomatch@4.0.2)
|
||||
picomatch: 4.0.2
|
||||
|
||||
tlds@1.255.0: {}
|
||||
|
||||
tmp@0.0.33:
|
||||
dependencies:
|
||||
os-tmpdir: 1.0.2
|
||||
|
@ -4181,6 +4506,8 @@ snapshots:
|
|||
media-typer: 0.3.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
uc.micro@2.1.0: {}
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
@ -4205,10 +4532,16 @@ snapshots:
|
|||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
utf7@1.0.2:
|
||||
dependencies:
|
||||
semver: 5.3.0
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
varint@6.0.0: {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
onlyBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
- better-sqlite3
|
||||
- esbuild
|
||||
- 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
|
||||
framework: {
|
||||
config: {
|
||||
dark: "auto"
|
||||
dark: true
|
||||
},
|
||||
|
||||
// iconSet: 'material-icons', // Quasar icon set
|
||||
|
|
|
@ -1,110 +1,14 @@
|
|||
import Database from 'better-sqlite3';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs'; // Needed to check if db file exists
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Determine the database path relative to this file
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const dbPath = join(__dirname, 'forms.db');
|
||||
// Instantiate Prisma Client
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
let db = null;
|
||||
// Export the Prisma Client instance for use in other modules
|
||||
export default prisma;
|
||||
|
||||
export function initializeDatabase() {
|
||||
if (db) {
|
||||
return db;
|
||||
}
|
||||
// --- Old better-sqlite3 code removed ---
|
||||
// No need for initializeDatabase, getDb, closeDatabase, etc.
|
||||
// Prisma Client manages the connection pool.
|
||||
|
||||
try {
|
||||
// Check if the directory exists, create if not (better-sqlite3 might need this)
|
||||
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.');
|
||||
}
|
||||
}
|
||||
// --- Settings Functions removed ---
|
||||
// Settings can now be accessed via prisma.setting.findUnique, prisma.setting.upsert, etc.
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,9 +19,10 @@ import {
|
|||
defineSsrRenderPreloadTag
|
||||
} from '#q-app/wrappers'
|
||||
|
||||
// Import database initialization and close function
|
||||
import { initializeDatabase, closeDatabase } from './database.js';
|
||||
import prisma from './database.js'; // Import the prisma client instance
|
||||
import apiRoutes from './routes/api.js';
|
||||
import cron from 'node-cron';
|
||||
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||
|
||||
/**
|
||||
* Create your webserver and return its instance.
|
||||
|
@ -35,12 +36,31 @@ export const create = defineSsrCreate((/* { ... } */) => {
|
|||
|
||||
// Initialize the database (now synchronous)
|
||||
try {
|
||||
initializeDatabase();
|
||||
console.log('Database initialized successfully.');
|
||||
console.log('Prisma Client is ready.'); // Log Prisma readiness
|
||||
|
||||
// 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('Failed to initialize database:', 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) {
|
||||
console.error('Error during server setup:', error);
|
||||
// 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
|
||||
|
@ -91,9 +111,14 @@ export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
|
|||
*
|
||||
* 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
|
||||
closeDatabase();
|
||||
try {
|
||||
await prisma.$disconnect();
|
||||
console.log('Prisma Client disconnected.');
|
||||
} catch (e) {
|
||||
console.error('Error disconnecting Prisma Client:', e);
|
||||
}
|
||||
|
||||
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-section>
|
||||
</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-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/: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/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