From eb0a0d77d66f2c37694dbb333b21ae50d39eaeed Mon Sep 17 00:00:00 2001
From: chamikaJ
Date: Thu, 16 May 2024 15:14:22 +0530
Subject: [PATCH 1/5] docs: updated image width
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 3ec6476f..3f42cbe2 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
From 298ca6beeb2679800bad0912a4ef8af7bfaa55c3 Mon Sep 17 00:00:00 2001
From: chamikaJ
Date: Fri, 17 May 2024 09:32:30 +0530
Subject: [PATCH 2/5] Initial commit: Angular frontend and Expressjs backend
---
SETUP_THE_PROJECT.md | 11 +-
worklenz-backend/.dockerignore | 5 +
worklenz-backend/.editorconfig | 19 +
worklenz-backend/.env.template | 55 +
worklenz-backend/.eslintrc.json | 109 +
worklenz-backend/.gitignore | 65 +
worklenz-backend/.gitmodules | 3 +
worklenz-backend/.npmrc | 2 +
worklenz-backend/Dockerfile | 26 +
worklenz-backend/Gruntfile.js | 131 +
worklenz-backend/README.md | 81 +
worklenz-backend/appspec.yml | 18 +
worklenz-backend/babel.config.js | 6 +
worklenz-backend/build.sh | 3 +
worklenz-backend/cli/esbuild-patch | 16 +
worklenz-backend/cli/generate-controller | 106 +
worklenz-backend/cli/generate-validator | 27 +
worklenz-backend/cli/inline-queries | 19 +
worklenz-backend/cli/mkrelease | 111 +
worklenz-backend/cli/swagger | 21 +
worklenz-backend/database/1_tables.sql | 1901 ++
worklenz-backend/database/2_triggers.sql | 161 +
worklenz-backend/database/3_system-data.sql | 77 +
worklenz-backend/database/4_views.sql | 34 +
worklenz-backend/database/5_functions.sql | 5791 ++++
.../database/6_user-permission.sql | 31 +
worklenz-backend/database/README.md | 1 +
worklenz-backend/doc/Database.md | 3 +
worklenz-backend/esbuild.js | 38 +
worklenz-backend/grunt/grunt-compress.js | 28 +
worklenz-backend/jest.config.js | 196 +
worklenz-backend/new | 78 +
worklenz-backend/package-lock.json | 16333 +++++++++++
worklenz-backend/package.json | 147 +
worklenz-backend/release | 1 +
worklenz-backend/scss/style.scss | 0
worklenz-backend/sonar-project.properties | 10 +
worklenz-backend/src/app.ts | 176 +
worklenz-backend/src/bin/config.ts | 6 +
worklenz-backend/src/bin/www.ts | 119 +
worklenz-backend/src/config/db-config.ts | 9 +
worklenz-backend/src/config/db.ts | 15 +
worklenz-backend/src/controllers/.gitkeep | 0
.../controllers/access-controls-controller.ts | 15 +
.../controllers/activity-logs-controller.ts | 34 +
.../controllers/admin-center-controller.ts | 309 +
.../src/controllers/attachment-controller.ts | 163 +
.../src/controllers/auth-controller.ts | 141 +
.../src/controllers/aws-ses-controller.ts | 58 +
.../src/controllers/clients-controller.ts | 81 +
.../src/controllers/gantt-controller.ts | 97 +
.../src/controllers/home-page-controller.ts | 422 +
.../src/controllers/index-controller.ts | 102 +
.../src/controllers/job-titles-controller.ts | 62 +
.../src/controllers/labels-controller.ts | 92 +
.../src/controllers/logs-controller.ts | 26 +
.../controllers/notification-controller.ts | 151 +
.../src/controllers/overview-controller.ts | 50 +
.../personal-overview-controller.ts | 72 +
.../profile-settings-controller.ts | 66 +
.../project-categories-controller.ts | 97 +
.../project-comments-controller.ts | 241 +
.../controllers/project-folders-controller.ts | 103 +
.../controllers/project-healths-controller.ts | 16 +
.../project-insights-controller.ts | 343 +
.../project-managers-controller.ts | 45 +
.../controllers/project-members-controller.ts | 150 +
.../roadmap-tasks-contoller-v2-base.ts | 77 +
.../roadmap-tasks-controller-v2.ts | 353 +
.../project-statuses-controller.ts | 16 +
.../project-templates/interfaces.ts | 88 +
.../project-templates-base.ts | 521 +
.../project-templates/project-templates.ts | 152 +
.../pt-task-phases-controller.ts | 104 +
.../pt-task-statuses-controller.ts | 146 +
.../pt-tasks-controller-base.ts | 56 +
.../project-templates/pt-tasks-controller.ts | 249 +
.../pt-templates-controller.ts | 233 +
.../project-workload/workload-gannt-base.ts | 64 +
.../workload-gannt-controller.ts | 844 +
.../src/controllers/projects-controller.ts | 744 +
.../src/controllers/reporting-controller.ts | 1844 ++
.../src/controllers/reporting/interfaces.ts | 60 +
.../overview/reporting-overview-base.ts | 993 +
.../overview/reporting-overview-controller.ts | 391 +
.../reporting-overview-export-controller.ts | 581 +
.../projects/reporting-projects-base.ts | 4 +
.../projects/reporting-projects-controller.ts | 218 +
.../reporting-projects-export-controller.ts | 279 +
.../reporting-allocation-controller.ts | 502 +
.../reporting/reporting-controller-base.ts | 604 +
.../reporting/reporting-info-controller.ts | 20 +
.../reporting/reporting-members-controller.ts | 1301 +
.../resource-allocation-controller.ts | 82 +
.../schedule/schedule-controller-base.ts | 64 +
.../schedule/schedule-controller.ts | 945 +
.../controllers/shared-projects-controller.ts | 61 +
.../src/controllers/sub-tasks-controller.ts | 119 +
.../controllers/task-comments-controller.ts | 265 +
.../task-list-columns-controller.ts | 39 +
.../src/controllers/task-phases-controller.ts | 119 +
.../controllers/task-priorities-controller.ts | 28 +
.../controllers/task-statuses-controller.ts | 161 +
.../controllers/task-templates-controller.ts | 79 +
.../controllers/task-work-log-controller.ts | 237 +
.../src/controllers/tasks-controller-base.ts | 91 +
.../src/controllers/tasks-controller-v2.ts | 482 +
.../src/controllers/tasks-controller.ts | 619 +
.../tasks-custom-columns-controller.ts | 52 +
.../controllers/team-members-controller.ts | 905 +
.../src/controllers/teams-controller.ts | 104 +
.../src/controllers/timezones-controller.ts | 23 +
.../src/controllers/todo-list-controller.ts | 88 +
.../controllers/worklenz-controller-base.ts | 69 +
.../src/cron_jobs/daily-digest-job.ts | 58 +
worklenz-backend/src/cron_jobs/helpers.ts | 75 +
worklenz-backend/src/cron_jobs/index.ts | 9 +
.../src/cron_jobs/notifications-job.ts | 68 +
.../src/cron_jobs/project-digest-job.ts | 70 +
.../src/decorators/handle-exceptions.ts | 83 +
worklenz-backend/src/interfaces/.gitkeep | 0
.../interfaces/aws-bounced-email-response.ts | 59 +
.../aws-complaint-email-response.ts | 42 +
.../interfaces/comment-email-notification.ts | 18 +
.../src/interfaces/daily-digest.ts | 13 +
.../src/interfaces/deserialize-callback.ts | 5 +
.../src/interfaces/email-template-type.ts | 16 +
.../src/interfaces/gantt-chart.ts | 24 +
.../src/interfaces/passport-session.ts | 20 +
.../interfaces/password-validity-result.ts | 6 +
.../src/interfaces/pg-session-data.ts | 12 +
.../src/interfaces/project-category.ts | 9 +
.../src/interfaces/project-digest.ts | 25 +
.../src/interfaces/project-folder.ts | 11 +
.../src/interfaces/serialize-callback.ts | 3 +
.../src/interfaces/socket-session.ts | 5 +
.../src/interfaces/task-assignments-model.ts | 30 +
.../src/interfaces/task-moved-to-done.ts | 14 +
worklenz-backend/src/interfaces/user.ts | 6 +
.../src/interfaces/worklenz-request.ts | 6 +
.../src/interfaces/worklenz-response.ts | 3 +
.../src/json_schemas/email-request-schema.ts | 13 +
.../src/json_schemas/item-create-schema.ts | 14 +
.../json_schemas/project-category-schema.ts | 13 +
.../src/json_schemas/project-folder-schema.ts | 13 +
.../json_schemas/task-phase-create-schema.ts | 12 +
.../src/middlewares/image-to-webp.ts | 23 +
.../src/middlewares/licensing-middleware.ts | 0
.../middlewares/map-tasks-to-bulk-update.ts | 9 +
.../src/middlewares/schema-validator.ts | 15 +
.../src/middlewares/session-middleware.ts | 26 +
.../src/middlewares/validators/README.md | 1 +
.../validators/avatar-validator.ts | 22 +
.../validators/body-name-validator.ts | 12 +
.../validators/bulk-tasks-phase-validators.ts | 13 +
.../bulk-tasks-priority-validators.ts | 13 +
.../validators/bulk-tasks-status-validator.ts | 13 +
.../validators/bulk-tasks-validator.ts | 15 +
.../validators/clients-body-validator.ts | 15 +
.../gantt-tasks-query-params-validator.ts | 12 +
.../gantt-tasks-range-params-validator.ts | 19 +
.../validators/home-task-body-validator.ts | 24 +
.../validators/id-param-validator.ts | 11 +
.../import-task-templates-validator.ts | 15 +
.../validators/job-titles-body-validator.ts | 15 +
.../kanban-status-update-validator.ts | 13 +
.../organization-owner-validator.ts | 11 +
.../organization-settings-validator.ts | 12 +
.../validators/password-validator.ts | 34 +
.../profile-settings-body-validator.ts | 12 +
.../validators/project-manager-validator.ts | 22 +
.../project-member-invite-validator.ts | 13 +
.../validators/project-member-validator.ts | 36 +
.../validators/projects-body-validator.ts | 34 +
.../pt-task-status-body-validator.ts | 22 +
.../validators/quick-task-validator.ts | 15 +
.../validators/reset-email-validator.ts | 17 +
.../middlewares/validators/setup-validator.ts | 34 +
.../shared-projects-create-validator.ts | 12 +
.../validators/sign-up-validator.ts | 13 +
.../validators/status-delete-validator.ts | 15 +
.../validators/status-order-validator.ts | 11 +
.../validators/task-attachments-validator.ts | 21 +
.../validators/task-comment-body-validator.ts | 21 +
.../validators/task-phase-name-validator.ts | 12 +
.../validators/task-status-body-validator.ts | 22 +
.../validators/task-time-log-validator.ts | 13 +
.../validators/tasks-body-validator.ts | 51 +
.../validators/team-members-body-validator.ts | 20 +
.../team-owner-or-admin-validator.ts | 11 +
.../team-settings-body-validator.ts | 12 +
.../teams-activate-body-validator.ts | 12 +
.../validators/todo-list-body-validator.ts | 16 +
.../validators/update-password-validator.ts | 15 +
.../middlewares/verify-project-membership.ts | 28 +
worklenz-backend/src/migrations/.gitkeep | 0
.../src/migrations/add-projects-keys.ts | 53 +
worklenz-backend/src/models/.gitkeep | 0
worklenz-backend/src/models/auth-response.ts | 17 +
.../src/models/reporting-export.ts | 166 +
.../src/models/server-response.ts | 22 +
worklenz-backend/src/passport/deserialize.ts | 42 +
worklenz-backend/src/passport/index.ts | 20 +
.../passport-strategies/passport-constants.ts | 2 +
.../passport-strategies/passport-google.ts | 73 +
.../passport-local-login.ts | 46 +
.../passport-local-signup.ts | 96 +
worklenz-backend/src/passport/serialize.ts | 7 +
.../db-task-status-changed.ts | 111 +
.../src/public/140.090f8f66847cac61.js | 1 +
.../src/public/148.3562c20f7c79dfbe.js | 1 +
.../src/public/150.aea42238d373936d.js | 1 +
.../src/public/152.89824b7edf9e2b3f.js | 129 +
.../src/public/169.3681839fd43f684a.js | 1 +
.../src/public/178.4e9f4f897f5f7737.js | 1 +
.../src/public/215.ab5a0edadc383dc1.js | 1 +
.../src/public/217.3eaf97d62e624659.js | 1 +
.../src/public/226.342b896c33d8d900.js | 1 +
.../src/public/265.e0fb54844d977984.js | 1 +
.../src/public/269.c4d92a972c5787a6.js | 1 +
.../src/public/31.366d3b5b3a40bd28.js | 1 +
.../src/public/3rdpartylicenses.txt | 1741 ++
.../src/public/406.ffcf05b3e5e7445d.js | 1 +
.../src/public/422.7a1aad3ba048190e.js | 1 +
.../src/public/450.2aa94132069852f3.js | 1 +
.../src/public/453.850082f04ceb33a4.js | 1 +
.../src/public/457.e5352d17b3d4ace6.js | 1 +
.../src/public/508.695e49a71f5042aa.js | 1 +
.../src/public/526.31f0f862d4cc0de0.js | 1 +
.../src/public/554.74cb9e7b0d19662d.js | 1 +
.../src/public/570.7a7379f081e3b4a7.js | 1 +
.../src/public/589.fc04e75bdb5d1681.js | 1 +
.../src/public/596.122034b2d91e32f1.js | 1 +
.../src/public/693.e0ca38056f3a2978.js | 1 +
.../src/public/771.c1c131496d7c6ea4.js | 1 +
.../src/public/787.c0da7f19a032adb5.js | 1 +
.../src/public/889.45a538df588b6347.js | 1 +
.../src/public/89.056b6bfe2710d324.js | 1 +
.../src/public/896.ca94baf9db3b3b4c.js | 1 +
.../src/public/921.09750d890ab4e8d5.js | 1 +
.../src/public/931.330457ea4270cc95.js | 1 +
.../src/public/940.48769e1469dae2f9.js | 1 +
.../src/public/945.9a0115568762948e.js | 1 +
.../src/public/assets/animal/panda.js | 16 +
.../src/public/assets/animal/panda.svg | 10 +
.../src/public/assets/css/prebuilt-editor.css | 16 +
.../src/public/assets/fill/.gitkeep | 0
.../src/public/assets/fill/account-book.js | 7 +
.../src/public/assets/fill/account-book.svg | 1 +
.../src/public/assets/fill/alert.js | 7 +
.../src/public/assets/fill/alert.svg | 1 +
.../src/public/assets/fill/alipay-circle.js | 7 +
.../src/public/assets/fill/alipay-circle.svg | 1 +
.../src/public/assets/fill/alipay-square.js | 7 +
.../src/public/assets/fill/alipay-square.svg | 1 +
.../src/public/assets/fill/aliwangwang.js | 7 +
.../src/public/assets/fill/aliwangwang.svg | 1 +
.../src/public/assets/fill/amazon-circle.js | 7 +
.../src/public/assets/fill/amazon-circle.svg | 1 +
.../src/public/assets/fill/amazon-square.js | 7 +
.../src/public/assets/fill/amazon-square.svg | 1 +
.../src/public/assets/fill/android.js | 7 +
.../src/public/assets/fill/android.svg | 1 +
.../src/public/assets/fill/api.js | 7 +
.../src/public/assets/fill/api.svg | 1 +
.../src/public/assets/fill/apple.js | 7 +
.../src/public/assets/fill/apple.svg | 1 +
.../src/public/assets/fill/appstore.js | 7 +
.../src/public/assets/fill/appstore.svg | 1 +
.../src/public/assets/fill/audio.js | 7 +
.../src/public/assets/fill/audio.svg | 1 +
.../src/public/assets/fill/backward.js | 7 +
.../src/public/assets/fill/backward.svg | 1 +
.../src/public/assets/fill/bank.js | 7 +
.../src/public/assets/fill/bank.svg | 1 +
.../src/public/assets/fill/behance-circle.js | 7 +
.../src/public/assets/fill/behance-circle.svg | 1 +
.../src/public/assets/fill/behance-square.js | 7 +
.../src/public/assets/fill/behance-square.svg | 1 +
.../src/public/assets/fill/bell.js | 7 +
.../src/public/assets/fill/bell.svg | 1 +
.../src/public/assets/fill/book.js | 7 +
.../src/public/assets/fill/book.svg | 1 +
.../src/public/assets/fill/box-plot.js | 7 +
.../src/public/assets/fill/box-plot.svg | 1 +
.../src/public/assets/fill/bug.js | 7 +
.../src/public/assets/fill/bug.svg | 1 +
.../src/public/assets/fill/build.js | 7 +
.../src/public/assets/fill/build.svg | 1 +
.../src/public/assets/fill/bulb.js | 7 +
.../src/public/assets/fill/bulb.svg | 1 +
.../src/public/assets/fill/calculator.js | 7 +
.../src/public/assets/fill/calculator.svg | 1 +
.../src/public/assets/fill/calendar.js | 7 +
.../src/public/assets/fill/calendar.svg | 1 +
.../src/public/assets/fill/camera.js | 7 +
.../src/public/assets/fill/camera.svg | 1 +
.../src/public/assets/fill/car.js | 7 +
.../src/public/assets/fill/car.svg | 1 +
.../src/public/assets/fill/caret-down.js | 7 +
.../src/public/assets/fill/caret-down.svg | 1 +
.../src/public/assets/fill/caret-left.js | 7 +
.../src/public/assets/fill/caret-left.svg | 1 +
.../src/public/assets/fill/caret-right.js | 7 +
.../src/public/assets/fill/caret-right.svg | 1 +
.../src/public/assets/fill/caret-up.js | 7 +
.../src/public/assets/fill/caret-up.svg | 1 +
.../src/public/assets/fill/carry-out.js | 7 +
.../src/public/assets/fill/carry-out.svg | 1 +
.../src/public/assets/fill/check-circle.js | 7 +
.../src/public/assets/fill/check-circle.svg | 1 +
.../src/public/assets/fill/check-square.js | 7 +
.../src/public/assets/fill/check-square.svg | 1 +
.../src/public/assets/fill/chrome.js | 7 +
.../src/public/assets/fill/chrome.svg | 1 +
.../src/public/assets/fill/ci-circle.js | 7 +
.../src/public/assets/fill/ci-circle.svg | 1 +
.../src/public/assets/fill/clock-circle.js | 7 +
.../src/public/assets/fill/clock-circle.svg | 1 +
.../src/public/assets/fill/close-circle.js | 7 +
.../src/public/assets/fill/close-circle.svg | 1 +
.../src/public/assets/fill/close-square.js | 7 +
.../src/public/assets/fill/close-square.svg | 1 +
.../src/public/assets/fill/cloud.js | 7 +
.../src/public/assets/fill/cloud.svg | 1 +
.../public/assets/fill/code-sandbox-circle.js | 7 +
.../assets/fill/code-sandbox-circle.svg | 1 +
.../public/assets/fill/code-sandbox-square.js | 7 +
.../assets/fill/code-sandbox-square.svg | 1 +
.../src/public/assets/fill/code.js | 7 +
.../src/public/assets/fill/code.svg | 1 +
.../src/public/assets/fill/codepen-circle.js | 7 +
.../src/public/assets/fill/codepen-circle.svg | 1 +
.../src/public/assets/fill/codepen-square.js | 7 +
.../src/public/assets/fill/codepen-square.svg | 1 +
.../src/public/assets/fill/compass.js | 7 +
.../src/public/assets/fill/compass.svg | 1 +
.../src/public/assets/fill/contacts.js | 7 +
.../src/public/assets/fill/contacts.svg | 1 +
.../src/public/assets/fill/container.js | 7 +
.../src/public/assets/fill/container.svg | 1 +
.../src/public/assets/fill/control.js | 7 +
.../src/public/assets/fill/control.svg | 1 +
.../src/public/assets/fill/copy.js | 7 +
.../src/public/assets/fill/copy.svg | 1 +
.../public/assets/fill/copyright-circle.js | 7 +
.../public/assets/fill/copyright-circle.svg | 1 +
.../src/public/assets/fill/credit-card.js | 7 +
.../src/public/assets/fill/credit-card.svg | 1 +
.../src/public/assets/fill/crown.js | 7 +
.../src/public/assets/fill/crown.svg | 1 +
.../public/assets/fill/customer-service.js | 7 +
.../public/assets/fill/customer-service.svg | 1 +
.../src/public/assets/fill/dashboard.js | 7 +
.../src/public/assets/fill/dashboard.svg | 1 +
.../src/public/assets/fill/database.js | 7 +
.../src/public/assets/fill/database.svg | 1 +
.../src/public/assets/fill/delete.js | 7 +
.../src/public/assets/fill/delete.svg | 1 +
.../src/public/assets/fill/diff.js | 7 +
.../src/public/assets/fill/diff.svg | 1 +
.../src/public/assets/fill/dingtalk-circle.js | 7 +
.../public/assets/fill/dingtalk-circle.svg | 1 +
.../src/public/assets/fill/dingtalk-square.js | 7 +
.../public/assets/fill/dingtalk-square.svg | 1 +
.../src/public/assets/fill/dislike.js | 7 +
.../src/public/assets/fill/dislike.svg | 1 +
.../src/public/assets/fill/dollar-circle.js | 7 +
.../src/public/assets/fill/dollar-circle.svg | 1 +
.../src/public/assets/fill/down-circle.js | 7 +
.../src/public/assets/fill/down-circle.svg | 1 +
.../src/public/assets/fill/down-square.js | 7 +
.../src/public/assets/fill/down-square.svg | 1 +
.../src/public/assets/fill/dribbble-circle.js | 7 +
.../public/assets/fill/dribbble-circle.svg | 1 +
.../src/public/assets/fill/dribbble-square.js | 7 +
.../public/assets/fill/dribbble-square.svg | 1 +
.../src/public/assets/fill/dropbox-circle.js | 7 +
.../src/public/assets/fill/dropbox-circle.svg | 1 +
.../src/public/assets/fill/dropbox-square.js | 7 +
.../src/public/assets/fill/dropbox-square.svg | 1 +
.../src/public/assets/fill/edit.js | 7 +
.../src/public/assets/fill/edit.svg | 1 +
.../src/public/assets/fill/environment.js | 7 +
.../src/public/assets/fill/environment.svg | 1 +
.../src/public/assets/fill/euro-circle.js | 7 +
.../src/public/assets/fill/euro-circle.svg | 1 +
.../public/assets/fill/exclamation-circle.js | 7 +
.../public/assets/fill/exclamation-circle.svg | 1 +
.../src/public/assets/fill/experiment.js | 7 +
.../src/public/assets/fill/experiment.svg | 1 +
.../src/public/assets/fill/eye-invisible.js | 7 +
.../src/public/assets/fill/eye-invisible.svg | 1 +
.../src/public/assets/fill/eye.js | 7 +
.../src/public/assets/fill/eye.svg | 1 +
.../src/public/assets/fill/facebook.js | 7 +
.../src/public/assets/fill/facebook.svg | 1 +
.../src/public/assets/fill/fast-backward.js | 7 +
.../src/public/assets/fill/fast-backward.svg | 1 +
.../src/public/assets/fill/fast-forward.js | 7 +
.../src/public/assets/fill/fast-forward.svg | 1 +
.../src/public/assets/fill/file-add.js | 7 +
.../src/public/assets/fill/file-add.svg | 1 +
.../src/public/assets/fill/file-excel.js | 7 +
.../src/public/assets/fill/file-excel.svg | 1 +
.../public/assets/fill/file-exclamation.js | 7 +
.../public/assets/fill/file-exclamation.svg | 1 +
.../src/public/assets/fill/file-image.js | 7 +
.../src/public/assets/fill/file-image.svg | 1 +
.../src/public/assets/fill/file-markdown.js | 7 +
.../src/public/assets/fill/file-markdown.svg | 1 +
.../src/public/assets/fill/file-pdf.js | 7 +
.../src/public/assets/fill/file-pdf.svg | 1 +
.../src/public/assets/fill/file-ppt.js | 7 +
.../src/public/assets/fill/file-ppt.svg | 1 +
.../src/public/assets/fill/file-text.js | 7 +
.../src/public/assets/fill/file-text.svg | 1 +
.../src/public/assets/fill/file-unknown.js | 7 +
.../src/public/assets/fill/file-unknown.svg | 1 +
.../src/public/assets/fill/file-word.js | 7 +
.../src/public/assets/fill/file-word.svg | 1 +
.../src/public/assets/fill/file-zip.js | 7 +
.../src/public/assets/fill/file-zip.svg | 1 +
.../src/public/assets/fill/file.js | 7 +
.../src/public/assets/fill/file.svg | 1 +
.../src/public/assets/fill/filter.js | 7 +
.../src/public/assets/fill/filter.svg | 1 +
.../src/public/assets/fill/fire.js | 7 +
.../src/public/assets/fill/fire.svg | 1 +
.../src/public/assets/fill/flag.js | 7 +
.../src/public/assets/fill/flag.svg | 1 +
.../src/public/assets/fill/folder-add.js | 7 +
.../src/public/assets/fill/folder-add.svg | 1 +
.../src/public/assets/fill/folder-open.js | 7 +
.../src/public/assets/fill/folder-open.svg | 1 +
.../src/public/assets/fill/folder.js | 7 +
.../src/public/assets/fill/folder.svg | 1 +
.../src/public/assets/fill/format-painter.js | 7 +
.../src/public/assets/fill/format-painter.svg | 1 +
.../src/public/assets/fill/forward.js | 7 +
.../src/public/assets/fill/forward.svg | 1 +
.../src/public/assets/fill/frown.js | 7 +
.../src/public/assets/fill/frown.svg | 1 +
.../src/public/assets/fill/fund.js | 7 +
.../src/public/assets/fill/fund.svg | 1 +
.../src/public/assets/fill/funnel-plot.js | 7 +
.../src/public/assets/fill/funnel-plot.svg | 1 +
.../src/public/assets/fill/gift.js | 7 +
.../src/public/assets/fill/gift.svg | 1 +
.../src/public/assets/fill/github.js | 7 +
.../src/public/assets/fill/github.svg | 1 +
.../src/public/assets/fill/gitlab.js | 7 +
.../src/public/assets/fill/gitlab.svg | 1 +
.../src/public/assets/fill/gold.js | 7 +
.../src/public/assets/fill/gold.svg | 1 +
.../src/public/assets/fill/golden.js | 7 +
.../src/public/assets/fill/golden.svg | 1 +
.../src/public/assets/fill/google-circle.js | 7 +
.../src/public/assets/fill/google-circle.svg | 1 +
.../public/assets/fill/google-plus-circle.js | 7 +
.../public/assets/fill/google-plus-circle.svg | 1 +
.../public/assets/fill/google-plus-square.js | 7 +
.../public/assets/fill/google-plus-square.svg | 1 +
.../src/public/assets/fill/google-square.js | 7 +
.../src/public/assets/fill/google-square.svg | 1 +
.../src/public/assets/fill/hdd.js | 7 +
.../src/public/assets/fill/hdd.svg | 1 +
.../src/public/assets/fill/heart.js | 7 +
.../src/public/assets/fill/heart.svg | 1 +
.../src/public/assets/fill/highlight.js | 7 +
.../src/public/assets/fill/highlight.svg | 1 +
.../src/public/assets/fill/home.js | 7 +
.../src/public/assets/fill/home.svg | 1 +
.../src/public/assets/fill/hourglass.js | 7 +
.../src/public/assets/fill/hourglass.svg | 1 +
.../src/public/assets/fill/html5.js | 7 +
.../src/public/assets/fill/html5.svg | 1 +
.../src/public/assets/fill/idcard.js | 7 +
.../src/public/assets/fill/idcard.svg | 1 +
.../src/public/assets/fill/ie-circle.js | 7 +
.../src/public/assets/fill/ie-circle.svg | 1 +
.../src/public/assets/fill/ie-square.js | 7 +
.../src/public/assets/fill/ie-square.svg | 1 +
.../src/public/assets/fill/info-circle.js | 7 +
.../src/public/assets/fill/info-circle.svg | 1 +
.../src/public/assets/fill/instagram.js | 7 +
.../src/public/assets/fill/instagram.svg | 1 +
.../src/public/assets/fill/insurance.js | 7 +
.../src/public/assets/fill/insurance.svg | 1 +
.../src/public/assets/fill/interaction.js | 7 +
.../src/public/assets/fill/interaction.svg | 1 +
.../src/public/assets/fill/layout.js | 7 +
.../src/public/assets/fill/layout.svg | 1 +
.../src/public/assets/fill/left-circle.js | 7 +
.../src/public/assets/fill/left-circle.svg | 1 +
.../src/public/assets/fill/left-square.js | 7 +
.../src/public/assets/fill/left-square.svg | 1 +
.../src/public/assets/fill/like.js | 7 +
.../src/public/assets/fill/like.svg | 1 +
.../src/public/assets/fill/linkedin.js | 7 +
.../src/public/assets/fill/linkedin.svg | 1 +
.../src/public/assets/fill/lock.js | 7 +
.../src/public/assets/fill/lock.svg | 1 +
.../src/public/assets/fill/mac-command.js | 7 +
.../src/public/assets/fill/mac-command.svg | 1 +
.../src/public/assets/fill/mail.js | 7 +
.../src/public/assets/fill/mail.svg | 1 +
.../src/public/assets/fill/medicine-box.js | 7 +
.../src/public/assets/fill/medicine-box.svg | 1 +
.../src/public/assets/fill/medium-circle.js | 7 +
.../src/public/assets/fill/medium-circle.svg | 1 +
.../src/public/assets/fill/medium-square.js | 7 +
.../src/public/assets/fill/medium-square.svg | 1 +
.../src/public/assets/fill/meh.js | 7 +
.../src/public/assets/fill/meh.svg | 1 +
.../src/public/assets/fill/message.js | 7 +
.../src/public/assets/fill/message.svg | 1 +
.../src/public/assets/fill/minus-circle.js | 7 +
.../src/public/assets/fill/minus-circle.svg | 1 +
.../src/public/assets/fill/minus-square.js | 7 +
.../src/public/assets/fill/minus-square.svg | 1 +
.../src/public/assets/fill/mobile.js | 7 +
.../src/public/assets/fill/mobile.svg | 1 +
.../src/public/assets/fill/money-collect.js | 7 +
.../src/public/assets/fill/money-collect.svg | 1 +
.../src/public/assets/fill/notification.js | 7 +
.../src/public/assets/fill/notification.svg | 1 +
.../src/public/assets/fill/pause-circle.js | 7 +
.../src/public/assets/fill/pause-circle.svg | 1 +
.../src/public/assets/fill/pay-circle.js | 7 +
.../src/public/assets/fill/pay-circle.svg | 1 +
.../src/public/assets/fill/phone.js | 7 +
.../src/public/assets/fill/phone.svg | 1 +
.../src/public/assets/fill/picture.js | 7 +
.../src/public/assets/fill/picture.svg | 1 +
.../src/public/assets/fill/pie-chart.js | 7 +
.../src/public/assets/fill/pie-chart.svg | 1 +
.../src/public/assets/fill/play-circle.js | 7 +
.../src/public/assets/fill/play-circle.svg | 1 +
.../src/public/assets/fill/play-square.js | 7 +
.../src/public/assets/fill/play-square.svg | 1 +
.../src/public/assets/fill/plus-circle.js | 7 +
.../src/public/assets/fill/plus-circle.svg | 1 +
.../src/public/assets/fill/plus-square.js | 7 +
.../src/public/assets/fill/plus-square.svg | 1 +
.../src/public/assets/fill/pound-circle.js | 7 +
.../src/public/assets/fill/pound-circle.svg | 1 +
.../src/public/assets/fill/printer.js | 7 +
.../src/public/assets/fill/printer.svg | 1 +
.../src/public/assets/fill/profile.js | 7 +
.../src/public/assets/fill/profile.svg | 1 +
.../src/public/assets/fill/project.js | 7 +
.../src/public/assets/fill/project.svg | 1 +
.../src/public/assets/fill/property-safety.js | 7 +
.../public/assets/fill/property-safety.svg | 1 +
.../src/public/assets/fill/pushpin.js | 7 +
.../src/public/assets/fill/pushpin.svg | 1 +
.../src/public/assets/fill/qq-circle.js | 7 +
.../src/public/assets/fill/qq-circle.svg | 1 +
.../src/public/assets/fill/qq-square.js | 7 +
.../src/public/assets/fill/qq-square.svg | 1 +
.../src/public/assets/fill/question-circle.js | 7 +
.../public/assets/fill/question-circle.svg | 1 +
.../src/public/assets/fill/read.js | 7 +
.../src/public/assets/fill/read.svg | 1 +
.../src/public/assets/fill/reconciliation.js | 7 +
.../src/public/assets/fill/reconciliation.svg | 1 +
.../src/public/assets/fill/red-envelope.js | 7 +
.../src/public/assets/fill/red-envelope.svg | 1 +
.../src/public/assets/fill/reddit-circle.js | 7 +
.../src/public/assets/fill/reddit-circle.svg | 1 +
.../src/public/assets/fill/reddit-square.js | 7 +
.../src/public/assets/fill/reddit-square.svg | 1 +
.../src/public/assets/fill/rest.js | 7 +
.../src/public/assets/fill/rest.svg | 1 +
.../src/public/assets/fill/right-circle.js | 7 +
.../src/public/assets/fill/right-circle.svg | 1 +
.../src/public/assets/fill/right-square.js | 7 +
.../src/public/assets/fill/right-square.svg | 1 +
.../src/public/assets/fill/robot.js | 7 +
.../src/public/assets/fill/robot.svg | 1 +
.../src/public/assets/fill/rocket.js | 7 +
.../src/public/assets/fill/rocket.svg | 1 +
.../public/assets/fill/safety-certificate.js | 7 +
.../public/assets/fill/safety-certificate.svg | 1 +
.../src/public/assets/fill/save.js | 7 +
.../src/public/assets/fill/save.svg | 1 +
.../src/public/assets/fill/schedule.js | 7 +
.../src/public/assets/fill/schedule.svg | 1 +
.../src/public/assets/fill/security-scan.js | 7 +
.../src/public/assets/fill/security-scan.svg | 1 +
.../src/public/assets/fill/setting.js | 7 +
.../src/public/assets/fill/setting.svg | 1 +
.../src/public/assets/fill/shop.js | 7 +
.../src/public/assets/fill/shop.svg | 1 +
.../src/public/assets/fill/shopping.js | 7 +
.../src/public/assets/fill/shopping.svg | 1 +
.../src/public/assets/fill/signal.js | 7 +
.../src/public/assets/fill/signal.svg | 1 +
.../src/public/assets/fill/sketch-circle.js | 7 +
.../src/public/assets/fill/sketch-circle.svg | 1 +
.../src/public/assets/fill/sketch-square.js | 7 +
.../src/public/assets/fill/sketch-square.svg | 1 +
.../src/public/assets/fill/skin.js | 7 +
.../src/public/assets/fill/skin.svg | 1 +
.../src/public/assets/fill/skype.js | 7 +
.../src/public/assets/fill/skype.svg | 1 +
.../src/public/assets/fill/slack-circle.js | 7 +
.../src/public/assets/fill/slack-circle.svg | 1 +
.../src/public/assets/fill/slack-square.js | 7 +
.../src/public/assets/fill/slack-square.svg | 1 +
.../src/public/assets/fill/sliders.js | 7 +
.../src/public/assets/fill/sliders.svg | 1 +
.../src/public/assets/fill/smile.js | 7 +
.../src/public/assets/fill/smile.svg | 1 +
.../src/public/assets/fill/snippets.js | 7 +
.../src/public/assets/fill/snippets.svg | 1 +
.../src/public/assets/fill/sound.js | 7 +
.../src/public/assets/fill/sound.svg | 1 +
.../src/public/assets/fill/star.js | 7 +
.../src/public/assets/fill/star.svg | 1 +
.../src/public/assets/fill/step-backward.js | 7 +
.../src/public/assets/fill/step-backward.svg | 1 +
.../src/public/assets/fill/step-forward.js | 7 +
.../src/public/assets/fill/step-forward.svg | 1 +
.../src/public/assets/fill/stop.js | 7 +
.../src/public/assets/fill/stop.svg | 1 +
.../src/public/assets/fill/switcher.js | 7 +
.../src/public/assets/fill/switcher.svg | 1 +
.../src/public/assets/fill/tablet.js | 7 +
.../src/public/assets/fill/tablet.svg | 1 +
.../src/public/assets/fill/tag.js | 7 +
.../src/public/assets/fill/tag.svg | 1 +
.../src/public/assets/fill/tags.js | 7 +
.../src/public/assets/fill/tags.svg | 1 +
.../src/public/assets/fill/taobao-circle.js | 7 +
.../src/public/assets/fill/taobao-circle.svg | 1 +
.../src/public/assets/fill/taobao-square.js | 7 +
.../src/public/assets/fill/taobao-square.svg | 1 +
.../src/public/assets/fill/thunderbolt.js | 7 +
.../src/public/assets/fill/thunderbolt.svg | 1 +
.../src/public/assets/fill/tool.js | 7 +
.../src/public/assets/fill/tool.svg | 1 +
.../public/assets/fill/trademark-circle.js | 7 +
.../public/assets/fill/trademark-circle.svg | 1 +
.../src/public/assets/fill/trophy.js | 7 +
.../src/public/assets/fill/trophy.svg | 1 +
.../src/public/assets/fill/twitter-circle.js | 7 +
.../src/public/assets/fill/twitter-circle.svg | 1 +
.../src/public/assets/fill/twitter-square.js | 7 +
.../src/public/assets/fill/twitter-square.svg | 1 +
.../src/public/assets/fill/unlock.js | 7 +
.../src/public/assets/fill/unlock.svg | 1 +
.../src/public/assets/fill/up-circle.js | 7 +
.../src/public/assets/fill/up-circle.svg | 1 +
.../src/public/assets/fill/up-square.js | 7 +
.../src/public/assets/fill/up-square.svg | 1 +
.../src/public/assets/fill/usb.js | 7 +
.../src/public/assets/fill/usb.svg | 1 +
.../src/public/assets/fill/video-camera.js | 7 +
.../src/public/assets/fill/video-camera.svg | 1 +
.../src/public/assets/fill/wallet.js | 7 +
.../src/public/assets/fill/wallet.svg | 1 +
.../src/public/assets/fill/warning.js | 7 +
.../src/public/assets/fill/warning.svg | 1 +
.../src/public/assets/fill/wechat.js | 7 +
.../src/public/assets/fill/wechat.svg | 1 +
.../src/public/assets/fill/weibo-circle.js | 7 +
.../src/public/assets/fill/weibo-circle.svg | 1 +
.../src/public/assets/fill/weibo-square.js | 7 +
.../src/public/assets/fill/weibo-square.svg | 1 +
.../src/public/assets/fill/windows.js | 7 +
.../src/public/assets/fill/windows.svg | 1 +
.../src/public/assets/fill/yahoo.js | 7 +
.../src/public/assets/fill/yahoo.svg | 1 +
.../src/public/assets/fill/youtube.js | 7 +
.../src/public/assets/fill/youtube.svg | 1 +
.../src/public/assets/fill/yuque.js | 7 +
.../src/public/assets/fill/yuque.svg | 1 +
.../src/public/assets/fill/zhihu-circle.js | 7 +
.../src/public/assets/fill/zhihu-circle.svg | 1 +
.../src/public/assets/fill/zhihu-square.js | 7 +
.../src/public/assets/fill/zhihu-square.svg | 1 +
.../src/public/assets/icons/icon-128x128.png | Bin 0 -> 8396 bytes
.../src/public/assets/icons/icon-144x144.png | Bin 0 -> 9344 bytes
.../src/public/assets/icons/icon-152x152.png | Bin 0 -> 9823 bytes
.../src/public/assets/icons/icon-192x192.png | Bin 0 -> 12077 bytes
.../src/public/assets/icons/icon-384x384.png | Bin 0 -> 26068 bytes
.../src/public/assets/icons/icon-512x512.png | Bin 0 -> 29712 bytes
.../src/public/assets/icons/icon-72x72.png | Bin 0 -> 5410 bytes
.../src/public/assets/icons/icon-96x96.png | Bin 0 -> 6682 bytes
.../src/public/assets/images/404.svg | 325 +
.../src/public/assets/images/block-user.png | Bin 0 -> 44000 bytes
.../assets/images/chevron-down-solid.svg | 4 +
.../src/public/assets/images/clipboard.png | Bin 0 -> 20743 bytes
.../src/public/assets/images/clock-green.png | Bin 0 -> 4981 bytes
.../src/public/assets/images/clock-red.png | Bin 0 -> 4922 bytes
.../src/public/assets/images/clock-yellow.png | Bin 0 -> 46382 bytes
.../src/public/assets/images/confetti (1).png | Bin 0 -> 20398 bytes
.../src/public/assets/images/confetti.png | Bin 0 -> 43336 bytes
.../src/public/assets/images/empty-box.png | Bin 0 -> 33801 bytes
.../src/public/assets/images/empty-box.webp | Bin 0 -> 19842 bytes
.../src/public/assets/images/files/ai.png | Bin 0 -> 10687 bytes
.../src/public/assets/images/files/avi.png | Bin 0 -> 7782 bytes
.../src/public/assets/images/files/css.png | Bin 0 -> 12761 bytes
.../src/public/assets/images/files/csv.png | Bin 0 -> 10053 bytes
.../src/public/assets/images/files/doc.png | Bin 0 -> 12689 bytes
.../src/public/assets/images/files/exe.png | Bin 0 -> 11570 bytes
.../src/public/assets/images/files/html.png | Bin 0 -> 6723 bytes
.../src/public/assets/images/files/jpg.png | Bin 0 -> 12146 bytes
.../src/public/assets/images/files/js.png | Bin 0 -> 11050 bytes
.../src/public/assets/images/files/json.png | Bin 0 -> 13972 bytes
.../src/public/assets/images/files/mp3.png | Bin 0 -> 13511 bytes
.../src/public/assets/images/files/mp4.png | Bin 0 -> 14697 bytes
.../src/public/assets/images/files/pdf.png | Bin 0 -> 13096 bytes
.../src/public/assets/images/files/png.png | Bin 0 -> 12544 bytes
.../src/public/assets/images/files/ppt.png | Bin 0 -> 6475 bytes
.../src/public/assets/images/files/psd.png | Bin 0 -> 12781 bytes
.../src/public/assets/images/files/search.png | Bin 0 -> 12353 bytes
.../src/public/assets/images/files/svg.png | Bin 0 -> 12957 bytes
.../src/public/assets/images/files/txt.png | Bin 0 -> 9600 bytes
.../src/public/assets/images/files/xls.png | Bin 0 -> 8267 bytes
.../src/public/assets/images/files/xml.png | Bin 0 -> 11677 bytes
.../src/public/assets/images/files/zip.png | Bin 0 -> 7376 bytes
.../src/public/assets/images/folder.svg | 5 +
.../src/public/assets/images/google-icon.png | Bin 0 -> 10275 bytes
.../public/assets/images/google_sign_in.png | Bin 0 -> 4308 bytes
.../src/public/assets/images/graph-report.png | Bin 0 -> 38597 bytes
.../src/public/assets/images/group.png | Bin 0 -> 51416 bytes
.../src/public/assets/images/group_1.png | Bin 0 -> 49239 bytes
.../assets/images/insights-calendar.png | Bin 0 -> 19138 bytes
.../public/assets/images/insights-check.png | Bin 0 -> 24398 bytes
.../src/public/assets/images/logo-lg.png | Bin 0 -> 230748 bytes
.../src/public/assets/images/logo-sm.png | Bin 0 -> 4803 bytes
.../src/public/assets/images/logo.png | Bin 0 -> 28062 bytes
.../assets/images/magnifying-glass-solid.svg | 5 +
.../src/public/assets/images/noimage.png | Bin 0 -> 15076 bytes
.../src/public/assets/images/open-icon.svg | 5 +
.../src/public/assets/images/progress.png | Bin 0 -> 24159 bytes
.../assets/images/project-management.png | Bin 0 -> 19755 bytes
.../src/public/assets/images/team-members.svg | 11 +
.../src/public/assets/images/to-do-list.png | Bin 0 -> 29396 bytes
.../src/public/assets/images/user.png | Bin 0 -> 22004 bytes
.../src/public/assets/images/warning.png | Bin 0 -> 21073 bytes
.../src/public/assets/outline/.gitkeep | 0
.../src/public/assets/outline/account-book.js | 7 +
.../public/assets/outline/account-book.svg | 1 +
.../src/public/assets/outline/aim.js | 7 +
.../src/public/assets/outline/aim.svg | 1 +
.../src/public/assets/outline/alert.js | 7 +
.../src/public/assets/outline/alert.svg | 1 +
.../src/public/assets/outline/alibaba.js | 7 +
.../src/public/assets/outline/alibaba.svg | 1 +
.../src/public/assets/outline/align-center.js | 7 +
.../public/assets/outline/align-center.svg | 1 +
.../src/public/assets/outline/align-left.js | 7 +
.../src/public/assets/outline/align-left.svg | 1 +
.../src/public/assets/outline/align-right.js | 7 +
.../src/public/assets/outline/align-right.svg | 1 +
.../public/assets/outline/alipay-circle.js | 7 +
.../public/assets/outline/alipay-circle.svg | 1 +
.../src/public/assets/outline/alipay.js | 7 +
.../src/public/assets/outline/alipay.svg | 1 +
.../src/public/assets/outline/aliwangwang.js | 7 +
.../src/public/assets/outline/aliwangwang.svg | 1 +
.../src/public/assets/outline/aliyun.js | 7 +
.../src/public/assets/outline/aliyun.svg | 1 +
.../src/public/assets/outline/amazon.js | 7 +
.../src/public/assets/outline/amazon.svg | 1 +
.../src/public/assets/outline/android.js | 7 +
.../src/public/assets/outline/android.svg | 1 +
.../src/public/assets/outline/ant-cloud.js | 7 +
.../src/public/assets/outline/ant-cloud.svg | 1 +
.../src/public/assets/outline/ant-design.js | 7 +
.../src/public/assets/outline/ant-design.svg | 1 +
.../src/public/assets/outline/apartment.js | 7 +
.../src/public/assets/outline/apartment.svg | 1 +
.../src/public/assets/outline/api.js | 7 +
.../src/public/assets/outline/api.svg | 1 +
.../src/public/assets/outline/apple.js | 7 +
.../src/public/assets/outline/apple.svg | 1 +
.../src/public/assets/outline/appstore-add.js | 7 +
.../public/assets/outline/appstore-add.svg | 1 +
.../src/public/assets/outline/appstore.js | 7 +
.../src/public/assets/outline/appstore.svg | 1 +
.../src/public/assets/outline/area-chart.js | 7 +
.../src/public/assets/outline/area-chart.svg | 1 +
.../src/public/assets/outline/arrow-down.js | 7 +
.../src/public/assets/outline/arrow-down.svg | 1 +
.../src/public/assets/outline/arrow-left.js | 7 +
.../src/public/assets/outline/arrow-left.svg | 1 +
.../src/public/assets/outline/arrow-right.js | 7 +
.../src/public/assets/outline/arrow-right.svg | 1 +
.../src/public/assets/outline/arrow-up.js | 7 +
.../src/public/assets/outline/arrow-up.svg | 1 +
.../src/public/assets/outline/arrows-alt.js | 7 +
.../src/public/assets/outline/arrows-alt.svg | 1 +
.../src/public/assets/outline/audio-muted.js | 7 +
.../src/public/assets/outline/audio-muted.svg | 1 +
.../src/public/assets/outline/audio.js | 7 +
.../src/public/assets/outline/audio.svg | 1 +
.../src/public/assets/outline/audit.js | 7 +
.../src/public/assets/outline/audit.svg | 1 +
.../src/public/assets/outline/backward.js | 7 +
.../src/public/assets/outline/backward.svg | 1 +
.../src/public/assets/outline/bank.js | 7 +
.../src/public/assets/outline/bank.svg | 1 +
.../src/public/assets/outline/bar-chart.js | 7 +
.../src/public/assets/outline/bar-chart.svg | 1 +
.../src/public/assets/outline/barcode.js | 7 +
.../src/public/assets/outline/barcode.svg | 1 +
.../src/public/assets/outline/bars.js | 7 +
.../src/public/assets/outline/bars.svg | 1 +
.../public/assets/outline/behance-square.js | 7 +
.../public/assets/outline/behance-square.svg | 1 +
.../src/public/assets/outline/behance.js | 7 +
.../src/public/assets/outline/behance.svg | 1 +
.../src/public/assets/outline/bell.js | 7 +
.../src/public/assets/outline/bell.svg | 1 +
.../src/public/assets/outline/bg-colors.js | 7 +
.../src/public/assets/outline/bg-colors.svg | 1 +
.../src/public/assets/outline/block.js | 7 +
.../src/public/assets/outline/block.svg | 1 +
.../src/public/assets/outline/bold.js | 7 +
.../src/public/assets/outline/bold.svg | 1 +
.../src/public/assets/outline/book.js | 7 +
.../src/public/assets/outline/book.svg | 1 +
.../public/assets/outline/border-bottom.js | 7 +
.../public/assets/outline/border-bottom.svg | 1 +
.../assets/outline/border-horizontal.js | 7 +
.../assets/outline/border-horizontal.svg | 1 +
.../src/public/assets/outline/border-inner.js | 7 +
.../public/assets/outline/border-inner.svg | 1 +
.../src/public/assets/outline/border-left.js | 7 +
.../src/public/assets/outline/border-left.svg | 1 +
.../src/public/assets/outline/border-outer.js | 7 +
.../public/assets/outline/border-outer.svg | 1 +
.../src/public/assets/outline/border-right.js | 7 +
.../public/assets/outline/border-right.svg | 1 +
.../src/public/assets/outline/border-top.js | 7 +
.../src/public/assets/outline/border-top.svg | 1 +
.../public/assets/outline/border-verticle.js | 7 +
.../public/assets/outline/border-verticle.svg | 1 +
.../src/public/assets/outline/border.js | 7 +
.../src/public/assets/outline/border.svg | 1 +
.../public/assets/outline/borderless-table.js | 7 +
.../assets/outline/borderless-table.svg | 1 +
.../src/public/assets/outline/box-plot.js | 7 +
.../src/public/assets/outline/box-plot.svg | 1 +
.../src/public/assets/outline/branches.js | 7 +
.../src/public/assets/outline/branches.svg | 1 +
.../src/public/assets/outline/bug.js | 7 +
.../src/public/assets/outline/bug.svg | 1 +
.../src/public/assets/outline/build.js | 7 +
.../src/public/assets/outline/build.svg | 1 +
.../src/public/assets/outline/bulb.js | 7 +
.../src/public/assets/outline/bulb.svg | 1 +
.../src/public/assets/outline/calculator.js | 7 +
.../src/public/assets/outline/calculator.svg | 1 +
.../src/public/assets/outline/calendar.js | 7 +
.../src/public/assets/outline/calendar.svg | 1 +
.../src/public/assets/outline/camera.js | 7 +
.../src/public/assets/outline/camera.svg | 1 +
.../src/public/assets/outline/car.js | 7 +
.../src/public/assets/outline/car.svg | 1 +
.../src/public/assets/outline/caret-down.js | 7 +
.../src/public/assets/outline/caret-down.svg | 1 +
.../src/public/assets/outline/caret-left.js | 7 +
.../src/public/assets/outline/caret-left.svg | 1 +
.../src/public/assets/outline/caret-right.js | 7 +
.../src/public/assets/outline/caret-right.svg | 1 +
.../src/public/assets/outline/caret-up.js | 7 +
.../src/public/assets/outline/caret-up.svg | 1 +
.../src/public/assets/outline/carry-out.js | 7 +
.../src/public/assets/outline/carry-out.svg | 1 +
.../src/public/assets/outline/check-circle.js | 7 +
.../public/assets/outline/check-circle.svg | 1 +
.../src/public/assets/outline/check-square.js | 7 +
.../public/assets/outline/check-square.svg | 1 +
.../src/public/assets/outline/check.js | 7 +
.../src/public/assets/outline/check.svg | 1 +
.../src/public/assets/outline/chrome.js | 7 +
.../src/public/assets/outline/chrome.svg | 1 +
.../src/public/assets/outline/ci-circle.js | 7 +
.../src/public/assets/outline/ci-circle.svg | 1 +
.../src/public/assets/outline/ci.js | 7 +
.../src/public/assets/outline/ci.svg | 1 +
.../src/public/assets/outline/clear.js | 7 +
.../src/public/assets/outline/clear.svg | 1 +
.../src/public/assets/outline/clock-circle.js | 7 +
.../public/assets/outline/clock-circle.svg | 1 +
.../src/public/assets/outline/close-circle.js | 7 +
.../public/assets/outline/close-circle.svg | 1 +
.../src/public/assets/outline/close-square.js | 7 +
.../public/assets/outline/close-square.svg | 1 +
.../src/public/assets/outline/close.js | 7 +
.../src/public/assets/outline/close.svg | 1 +
.../public/assets/outline/cloud-download.js | 7 +
.../public/assets/outline/cloud-download.svg | 1 +
.../src/public/assets/outline/cloud-server.js | 7 +
.../public/assets/outline/cloud-server.svg | 1 +
.../src/public/assets/outline/cloud-sync.js | 7 +
.../src/public/assets/outline/cloud-sync.svg | 1 +
.../src/public/assets/outline/cloud-upload.js | 7 +
.../public/assets/outline/cloud-upload.svg | 1 +
.../src/public/assets/outline/cloud.js | 7 +
.../src/public/assets/outline/cloud.svg | 1 +
.../src/public/assets/outline/cluster.js | 7 +
.../src/public/assets/outline/cluster.svg | 1 +
.../src/public/assets/outline/code-sandbox.js | 7 +
.../public/assets/outline/code-sandbox.svg | 1 +
.../src/public/assets/outline/code.js | 7 +
.../src/public/assets/outline/code.svg | 1 +
.../public/assets/outline/codepen-circle.js | 7 +
.../public/assets/outline/codepen-circle.svg | 1 +
.../src/public/assets/outline/codepen.js | 7 +
.../src/public/assets/outline/codepen.svg | 1 +
.../src/public/assets/outline/coffee.js | 7 +
.../src/public/assets/outline/coffee.svg | 1 +
.../public/assets/outline/column-height.js | 7 +
.../public/assets/outline/column-height.svg | 1 +
.../src/public/assets/outline/column-width.js | 7 +
.../public/assets/outline/column-width.svg | 1 +
.../src/public/assets/outline/comment.js | 7 +
.../src/public/assets/outline/comment.svg | 1 +
.../src/public/assets/outline/compass.js | 7 +
.../src/public/assets/outline/compass.svg | 1 +
.../src/public/assets/outline/compress.js | 7 +
.../src/public/assets/outline/compress.svg | 1 +
.../src/public/assets/outline/console-sql.js | 7 +
.../src/public/assets/outline/console-sql.svg | 1 +
.../src/public/assets/outline/contacts.js | 7 +
.../src/public/assets/outline/contacts.svg | 1 +
.../src/public/assets/outline/container.js | 7 +
.../src/public/assets/outline/container.svg | 1 +
.../src/public/assets/outline/control.js | 7 +
.../src/public/assets/outline/control.svg | 1 +
.../src/public/assets/outline/copy.js | 7 +
.../src/public/assets/outline/copy.svg | 1 +
.../public/assets/outline/copyright-circle.js | 7 +
.../assets/outline/copyright-circle.svg | 1 +
.../src/public/assets/outline/copyright.js | 7 +
.../src/public/assets/outline/copyright.svg | 1 +
.../src/public/assets/outline/credit-card.js | 7 +
.../src/public/assets/outline/credit-card.svg | 1 +
.../src/public/assets/outline/crown.js | 7 +
.../src/public/assets/outline/crown.svg | 1 +
.../public/assets/outline/customer-service.js | 7 +
.../assets/outline/customer-service.svg | 1 +
.../src/public/assets/outline/dash.js | 7 +
.../src/public/assets/outline/dash.svg | 1 +
.../src/public/assets/outline/dashboard.js | 7 +
.../src/public/assets/outline/dashboard.svg | 1 +
.../src/public/assets/outline/database.js | 7 +
.../src/public/assets/outline/database.svg | 1 +
.../public/assets/outline/delete-column.js | 7 +
.../public/assets/outline/delete-column.svg | 1 +
.../src/public/assets/outline/delete-row.js | 7 +
.../src/public/assets/outline/delete-row.svg | 1 +
.../src/public/assets/outline/delete.js | 7 +
.../src/public/assets/outline/delete.svg | 1 +
.../assets/outline/delivered-procedure.js | 7 +
.../assets/outline/delivered-procedure.svg | 1 +
.../public/assets/outline/deployment-unit.js | 7 +
.../public/assets/outline/deployment-unit.svg | 1 +
.../src/public/assets/outline/desktop.js | 7 +
.../src/public/assets/outline/desktop.svg | 1 +
.../src/public/assets/outline/diff.js | 7 +
.../src/public/assets/outline/diff.svg | 1 +
.../src/public/assets/outline/dingding.js | 7 +
.../src/public/assets/outline/dingding.svg | 1 +
.../src/public/assets/outline/dingtalk.js | 7 +
.../src/public/assets/outline/dingtalk.svg | 1 +
.../src/public/assets/outline/disconnect.js | 7 +
.../src/public/assets/outline/disconnect.svg | 1 +
.../src/public/assets/outline/dislike.js | 7 +
.../src/public/assets/outline/dislike.svg | 1 +
.../public/assets/outline/dollar-circle.js | 7 +
.../public/assets/outline/dollar-circle.svg | 1 +
.../src/public/assets/outline/dollar.js | 7 +
.../src/public/assets/outline/dollar.svg | 1 +
.../src/public/assets/outline/dot-chart.js | 7 +
.../src/public/assets/outline/dot-chart.svg | 1 +
.../src/public/assets/outline/double-left.js | 7 +
.../src/public/assets/outline/double-left.svg | 1 +
.../src/public/assets/outline/double-right.js | 7 +
.../public/assets/outline/double-right.svg | 1 +
.../src/public/assets/outline/down-circle.js | 7 +
.../src/public/assets/outline/down-circle.svg | 1 +
.../src/public/assets/outline/down-square.js | 7 +
.../src/public/assets/outline/down-square.svg | 1 +
.../src/public/assets/outline/down.js | 7 +
.../src/public/assets/outline/down.svg | 1 +
.../src/public/assets/outline/download.js | 7 +
.../src/public/assets/outline/download.svg | 1 +
.../src/public/assets/outline/drag.js | 7 +
.../src/public/assets/outline/drag.svg | 1 +
.../public/assets/outline/dribbble-square.js | 7 +
.../public/assets/outline/dribbble-square.svg | 1 +
.../src/public/assets/outline/dribbble.js | 7 +
.../src/public/assets/outline/dribbble.svg | 1 +
.../src/public/assets/outline/dropbox.js | 7 +
.../src/public/assets/outline/dropbox.svg | 1 +
.../src/public/assets/outline/edit.js | 7 +
.../src/public/assets/outline/edit.svg | 1 +
.../src/public/assets/outline/ellipsis.js | 7 +
.../src/public/assets/outline/ellipsis.svg | 1 +
.../src/public/assets/outline/enter.js | 7 +
.../src/public/assets/outline/enter.svg | 1 +
.../src/public/assets/outline/environment.js | 7 +
.../src/public/assets/outline/environment.svg | 1 +
.../src/public/assets/outline/euro-circle.js | 7 +
.../src/public/assets/outline/euro-circle.svg | 1 +
.../src/public/assets/outline/euro.js | 7 +
.../src/public/assets/outline/euro.svg | 1 +
.../src/public/assets/outline/exception.js | 7 +
.../src/public/assets/outline/exception.svg | 1 +
.../assets/outline/exclamation-circle.js | 7 +
.../assets/outline/exclamation-circle.svg | 1 +
.../src/public/assets/outline/exclamation.js | 7 +
.../src/public/assets/outline/exclamation.svg | 1 +
.../src/public/assets/outline/expand-alt.js | 7 +
.../src/public/assets/outline/expand-alt.svg | 1 +
.../src/public/assets/outline/expand.js | 7 +
.../src/public/assets/outline/expand.svg | 1 +
.../src/public/assets/outline/experiment.js | 7 +
.../src/public/assets/outline/experiment.svg | 1 +
.../src/public/assets/outline/export.js | 7 +
.../src/public/assets/outline/export.svg | 1 +
.../public/assets/outline/eye-invisible.js | 7 +
.../public/assets/outline/eye-invisible.svg | 1 +
.../src/public/assets/outline/eye.js | 7 +
.../src/public/assets/outline/eye.svg | 1 +
.../src/public/assets/outline/facebook.js | 7 +
.../src/public/assets/outline/facebook.svg | 1 +
.../src/public/assets/outline/fall.js | 7 +
.../src/public/assets/outline/fall.svg | 1 +
.../public/assets/outline/fast-backward.js | 7 +
.../public/assets/outline/fast-backward.svg | 1 +
.../src/public/assets/outline/fast-forward.js | 7 +
.../public/assets/outline/fast-forward.svg | 1 +
.../src/public/assets/outline/field-binary.js | 7 +
.../public/assets/outline/field-binary.svg | 1 +
.../src/public/assets/outline/field-number.js | 7 +
.../public/assets/outline/field-number.svg | 1 +
.../src/public/assets/outline/field-string.js | 7 +
.../public/assets/outline/field-string.svg | 1 +
.../src/public/assets/outline/field-time.js | 7 +
.../src/public/assets/outline/field-time.svg | 1 +
.../src/public/assets/outline/file-add.js | 7 +
.../src/public/assets/outline/file-add.svg | 1 +
.../src/public/assets/outline/file-done.js | 7 +
.../src/public/assets/outline/file-done.svg | 1 +
.../src/public/assets/outline/file-excel.js | 7 +
.../src/public/assets/outline/file-excel.svg | 1 +
.../public/assets/outline/file-exclamation.js | 7 +
.../assets/outline/file-exclamation.svg | 1 +
.../src/public/assets/outline/file-gif.js | 7 +
.../src/public/assets/outline/file-gif.svg | 1 +
.../src/public/assets/outline/file-image.js | 7 +
.../src/public/assets/outline/file-image.svg | 1 +
.../src/public/assets/outline/file-jpg.js | 7 +
.../src/public/assets/outline/file-jpg.svg | 1 +
.../public/assets/outline/file-markdown.js | 7 +
.../public/assets/outline/file-markdown.svg | 1 +
.../src/public/assets/outline/file-pdf.js | 7 +
.../src/public/assets/outline/file-pdf.svg | 1 +
.../src/public/assets/outline/file-ppt.js | 7 +
.../src/public/assets/outline/file-ppt.svg | 1 +
.../src/public/assets/outline/file-protect.js | 7 +
.../public/assets/outline/file-protect.svg | 1 +
.../src/public/assets/outline/file-search.js | 7 +
.../src/public/assets/outline/file-search.svg | 1 +
.../src/public/assets/outline/file-sync.js | 7 +
.../src/public/assets/outline/file-sync.svg | 1 +
.../src/public/assets/outline/file-text.js | 7 +
.../src/public/assets/outline/file-text.svg | 1 +
.../src/public/assets/outline/file-unknown.js | 7 +
.../public/assets/outline/file-unknown.svg | 1 +
.../src/public/assets/outline/file-word.js | 7 +
.../src/public/assets/outline/file-word.svg | 1 +
.../src/public/assets/outline/file-zip.js | 7 +
.../src/public/assets/outline/file-zip.svg | 1 +
.../src/public/assets/outline/file.js | 7 +
.../src/public/assets/outline/file.svg | 1 +
.../src/public/assets/outline/filter.js | 7 +
.../src/public/assets/outline/filter.svg | 1 +
.../src/public/assets/outline/fire.js | 7 +
.../src/public/assets/outline/fire.svg | 1 +
.../src/public/assets/outline/flag.js | 7 +
.../src/public/assets/outline/flag.svg | 1 +
.../src/public/assets/outline/folder-add.js | 7 +
.../src/public/assets/outline/folder-add.svg | 1 +
.../src/public/assets/outline/folder-open.js | 7 +
.../src/public/assets/outline/folder-open.svg | 1 +
.../src/public/assets/outline/folder-view.js | 7 +
.../src/public/assets/outline/folder-view.svg | 1 +
.../src/public/assets/outline/folder.js | 7 +
.../src/public/assets/outline/folder.svg | 1 +
.../src/public/assets/outline/font-colors.js | 7 +
.../src/public/assets/outline/font-colors.svg | 1 +
.../src/public/assets/outline/font-size.js | 7 +
.../src/public/assets/outline/font-size.svg | 1 +
.../src/public/assets/outline/fork.js | 7 +
.../src/public/assets/outline/fork.svg | 1 +
.../src/public/assets/outline/form.js | 7 +
.../src/public/assets/outline/form.svg | 1 +
.../public/assets/outline/format-painter.js | 7 +
.../public/assets/outline/format-painter.svg | 1 +
.../src/public/assets/outline/forward.js | 7 +
.../src/public/assets/outline/forward.svg | 1 +
.../src/public/assets/outline/frown.js | 7 +
.../src/public/assets/outline/frown.svg | 1 +
.../public/assets/outline/fullscreen-exit.js | 7 +
.../public/assets/outline/fullscreen-exit.svg | 1 +
.../src/public/assets/outline/fullscreen.js | 7 +
.../src/public/assets/outline/fullscreen.svg | 1 +
.../src/public/assets/outline/function.js | 7 +
.../src/public/assets/outline/function.svg | 1 +
.../assets/outline/fund-projection-screen.js | 7 +
.../assets/outline/fund-projection-screen.svg | 1 +
.../src/public/assets/outline/fund-view.js | 7 +
.../src/public/assets/outline/fund-view.svg | 1 +
.../src/public/assets/outline/fund.js | 7 +
.../src/public/assets/outline/fund.svg | 1 +
.../src/public/assets/outline/funnel-plot.js | 7 +
.../src/public/assets/outline/funnel-plot.svg | 1 +
.../src/public/assets/outline/gateway.js | 7 +
.../src/public/assets/outline/gateway.svg | 1 +
.../src/public/assets/outline/gif.js | 7 +
.../src/public/assets/outline/gif.svg | 1 +
.../src/public/assets/outline/gift.js | 7 +
.../src/public/assets/outline/gift.svg | 1 +
.../src/public/assets/outline/github.js | 7 +
.../src/public/assets/outline/github.svg | 1 +
.../src/public/assets/outline/gitlab.js | 7 +
.../src/public/assets/outline/gitlab.svg | 1 +
.../src/public/assets/outline/global.js | 7 +
.../src/public/assets/outline/global.svg | 1 +
.../src/public/assets/outline/gold.js | 7 +
.../src/public/assets/outline/gold.svg | 1 +
.../src/public/assets/outline/google-plus.js | 7 +
.../src/public/assets/outline/google-plus.svg | 1 +
.../src/public/assets/outline/google.js | 7 +
.../src/public/assets/outline/google.svg | 1 +
.../src/public/assets/outline/group.js | 7 +
.../src/public/assets/outline/group.svg | 1 +
.../src/public/assets/outline/hdd.js | 7 +
.../src/public/assets/outline/hdd.svg | 1 +
.../src/public/assets/outline/heart.js | 7 +
.../src/public/assets/outline/heart.svg | 1 +
.../src/public/assets/outline/heat-map.js | 7 +
.../src/public/assets/outline/heat-map.svg | 1 +
.../src/public/assets/outline/highlight.js | 7 +
.../src/public/assets/outline/highlight.svg | 1 +
.../src/public/assets/outline/history.js | 7 +
.../src/public/assets/outline/history.svg | 1 +
.../src/public/assets/outline/holder.js | 7 +
.../src/public/assets/outline/holder.svg | 1 +
.../src/public/assets/outline/home.js | 7 +
.../src/public/assets/outline/home.svg | 1 +
.../src/public/assets/outline/hourglass.js | 7 +
.../src/public/assets/outline/hourglass.svg | 1 +
.../src/public/assets/outline/html5.js | 7 +
.../src/public/assets/outline/html5.svg | 1 +
.../src/public/assets/outline/idcard.js | 7 +
.../src/public/assets/outline/idcard.svg | 1 +
.../src/public/assets/outline/ie.js | 7 +
.../src/public/assets/outline/ie.svg | 1 +
.../src/public/assets/outline/import.js | 7 +
.../src/public/assets/outline/import.svg | 1 +
.../src/public/assets/outline/inbox.js | 7 +
.../src/public/assets/outline/inbox.svg | 1 +
.../src/public/assets/outline/info-circle.js | 7 +
.../src/public/assets/outline/info-circle.svg | 1 +
.../src/public/assets/outline/info.js | 7 +
.../src/public/assets/outline/info.svg | 1 +
.../public/assets/outline/insert-row-above.js | 7 +
.../assets/outline/insert-row-above.svg | 1 +
.../public/assets/outline/insert-row-below.js | 7 +
.../assets/outline/insert-row-below.svg | 1 +
.../public/assets/outline/insert-row-left.js | 7 +
.../public/assets/outline/insert-row-left.svg | 1 +
.../public/assets/outline/insert-row-right.js | 7 +
.../assets/outline/insert-row-right.svg | 1 +
.../src/public/assets/outline/instagram.js | 7 +
.../src/public/assets/outline/instagram.svg | 1 +
.../src/public/assets/outline/insurance.js | 7 +
.../src/public/assets/outline/insurance.svg | 1 +
.../src/public/assets/outline/interaction.js | 7 +
.../src/public/assets/outline/interaction.svg | 1 +
.../src/public/assets/outline/issues-close.js | 7 +
.../public/assets/outline/issues-close.svg | 1 +
.../src/public/assets/outline/italic.js | 7 +
.../src/public/assets/outline/italic.svg | 1 +
.../src/public/assets/outline/key.js | 7 +
.../src/public/assets/outline/key.svg | 1 +
.../src/public/assets/outline/laptop.js | 7 +
.../src/public/assets/outline/laptop.svg | 1 +
.../src/public/assets/outline/layout.js | 7 +
.../src/public/assets/outline/layout.svg | 1 +
.../src/public/assets/outline/left-circle.js | 7 +
.../src/public/assets/outline/left-circle.svg | 1 +
.../src/public/assets/outline/left-square.js | 7 +
.../src/public/assets/outline/left-square.svg | 1 +
.../src/public/assets/outline/left.js | 7 +
.../src/public/assets/outline/left.svg | 1 +
.../src/public/assets/outline/like.js | 7 +
.../src/public/assets/outline/like.svg | 1 +
.../src/public/assets/outline/line-chart.js | 7 +
.../src/public/assets/outline/line-chart.svg | 1 +
.../src/public/assets/outline/line-height.js | 7 +
.../src/public/assets/outline/line-height.svg | 1 +
.../src/public/assets/outline/line.js | 7 +
.../src/public/assets/outline/line.svg | 1 +
.../src/public/assets/outline/link.js | 7 +
.../src/public/assets/outline/link.svg | 1 +
.../src/public/assets/outline/linkedin.js | 7 +
.../src/public/assets/outline/linkedin.svg | 1 +
.../assets/outline/loading-3-quarters.js | 7 +
.../assets/outline/loading-3-quarters.svg | 1 +
.../src/public/assets/outline/loading.js | 7 +
.../src/public/assets/outline/loading.svg | 1 +
.../src/public/assets/outline/lock.js | 7 +
.../src/public/assets/outline/lock.svg | 1 +
.../src/public/assets/outline/login.js | 7 +
.../src/public/assets/outline/login.svg | 1 +
.../src/public/assets/outline/logout.js | 7 +
.../src/public/assets/outline/logout.svg | 1 +
.../src/public/assets/outline/mac-command.js | 7 +
.../src/public/assets/outline/mac-command.svg | 1 +
.../src/public/assets/outline/mail.js | 7 +
.../src/public/assets/outline/mail.svg | 1 +
.../src/public/assets/outline/man.js | 7 +
.../src/public/assets/outline/man.svg | 1 +
.../src/public/assets/outline/medicine-box.js | 7 +
.../public/assets/outline/medicine-box.svg | 1 +
.../public/assets/outline/medium-workmark.js | 7 +
.../public/assets/outline/medium-workmark.svg | 1 +
.../src/public/assets/outline/medium.js | 7 +
.../src/public/assets/outline/medium.svg | 1 +
.../src/public/assets/outline/meh.js | 7 +
.../src/public/assets/outline/meh.svg | 1 +
.../src/public/assets/outline/menu-fold.js | 7 +
.../src/public/assets/outline/menu-fold.svg | 1 +
.../src/public/assets/outline/menu-unfold.js | 7 +
.../src/public/assets/outline/menu-unfold.svg | 1 +
.../src/public/assets/outline/menu.js | 7 +
.../src/public/assets/outline/menu.svg | 1 +
.../src/public/assets/outline/merge-cells.js | 7 +
.../src/public/assets/outline/merge-cells.svg | 1 +
.../src/public/assets/outline/message.js | 7 +
.../src/public/assets/outline/message.svg | 1 +
.../src/public/assets/outline/minus-circle.js | 7 +
.../public/assets/outline/minus-circle.svg | 1 +
.../src/public/assets/outline/minus-square.js | 7 +
.../public/assets/outline/minus-square.svg | 1 +
.../src/public/assets/outline/minus.js | 7 +
.../src/public/assets/outline/minus.svg | 1 +
.../src/public/assets/outline/mobile.js | 7 +
.../src/public/assets/outline/mobile.svg | 1 +
.../public/assets/outline/money-collect.js | 7 +
.../public/assets/outline/money-collect.svg | 1 +
.../src/public/assets/outline/monitor.js | 7 +
.../src/public/assets/outline/monitor.svg | 1 +
.../src/public/assets/outline/more.js | 7 +
.../src/public/assets/outline/more.svg | 1 +
.../public/assets/outline/node-collapse.js | 7 +
.../public/assets/outline/node-collapse.svg | 1 +
.../src/public/assets/outline/node-expand.js | 7 +
.../src/public/assets/outline/node-expand.svg | 1 +
.../src/public/assets/outline/node-index.js | 7 +
.../src/public/assets/outline/node-index.svg | 1 +
.../src/public/assets/outline/notification.js | 7 +
.../public/assets/outline/notification.svg | 1 +
.../src/public/assets/outline/number.js | 7 +
.../src/public/assets/outline/number.svg | 1 +
.../src/public/assets/outline/one-to-one.js | 7 +
.../src/public/assets/outline/one-to-one.svg | 1 +
.../src/public/assets/outline/ordered-list.js | 7 +
.../public/assets/outline/ordered-list.svg | 1 +
.../src/public/assets/outline/paper-clip.js | 7 +
.../src/public/assets/outline/paper-clip.svg | 1 +
.../src/public/assets/outline/partition.js | 7 +
.../src/public/assets/outline/partition.svg | 1 +
.../src/public/assets/outline/pause-circle.js | 7 +
.../public/assets/outline/pause-circle.svg | 1 +
.../src/public/assets/outline/pause.js | 7 +
.../src/public/assets/outline/pause.svg | 1 +
.../src/public/assets/outline/pay-circle.js | 7 +
.../src/public/assets/outline/pay-circle.svg | 1 +
.../src/public/assets/outline/percentage.js | 7 +
.../src/public/assets/outline/percentage.svg | 1 +
.../src/public/assets/outline/phone.js | 7 +
.../src/public/assets/outline/phone.svg | 1 +
.../src/public/assets/outline/pic-center.js | 7 +
.../src/public/assets/outline/pic-center.svg | 1 +
.../src/public/assets/outline/pic-left.js | 7 +
.../src/public/assets/outline/pic-left.svg | 1 +
.../src/public/assets/outline/pic-right.js | 7 +
.../src/public/assets/outline/pic-right.svg | 1 +
.../src/public/assets/outline/picture.js | 7 +
.../src/public/assets/outline/picture.svg | 1 +
.../src/public/assets/outline/pie-chart.js | 7 +
.../src/public/assets/outline/pie-chart.svg | 1 +
.../src/public/assets/outline/play-circle.js | 7 +
.../src/public/assets/outline/play-circle.svg | 1 +
.../src/public/assets/outline/play-square.js | 7 +
.../src/public/assets/outline/play-square.svg | 1 +
.../src/public/assets/outline/plus-circle.js | 7 +
.../src/public/assets/outline/plus-circle.svg | 1 +
.../src/public/assets/outline/plus-square.js | 7 +
.../src/public/assets/outline/plus-square.svg | 1 +
.../src/public/assets/outline/plus.js | 7 +
.../src/public/assets/outline/plus.svg | 1 +
.../src/public/assets/outline/pound-circle.js | 7 +
.../public/assets/outline/pound-circle.svg | 1 +
.../src/public/assets/outline/pound.js | 7 +
.../src/public/assets/outline/pound.svg | 1 +
.../src/public/assets/outline/poweroff.js | 7 +
.../src/public/assets/outline/poweroff.svg | 1 +
.../src/public/assets/outline/printer.js | 7 +
.../src/public/assets/outline/printer.svg | 1 +
.../src/public/assets/outline/profile.js | 7 +
.../src/public/assets/outline/profile.svg | 1 +
.../src/public/assets/outline/project.js | 7 +
.../src/public/assets/outline/project.svg | 1 +
.../public/assets/outline/property-safety.js | 7 +
.../public/assets/outline/property-safety.svg | 1 +
.../src/public/assets/outline/pull-request.js | 7 +
.../public/assets/outline/pull-request.svg | 1 +
.../src/public/assets/outline/pushpin.js | 7 +
.../src/public/assets/outline/pushpin.svg | 1 +
.../src/public/assets/outline/qq.js | 7 +
.../src/public/assets/outline/qq.svg | 1 +
.../src/public/assets/outline/qrcode.js | 7 +
.../src/public/assets/outline/qrcode.svg | 1 +
.../public/assets/outline/question-circle.js | 7 +
.../public/assets/outline/question-circle.svg | 1 +
.../src/public/assets/outline/question.js | 7 +
.../src/public/assets/outline/question.svg | 1 +
.../src/public/assets/outline/radar-chart.js | 7 +
.../src/public/assets/outline/radar-chart.svg | 1 +
.../assets/outline/radius-bottomleft.js | 7 +
.../assets/outline/radius-bottomleft.svg | 1 +
.../assets/outline/radius-bottomright.js | 7 +
.../assets/outline/radius-bottomright.svg | 1 +
.../public/assets/outline/radius-setting.js | 7 +
.../public/assets/outline/radius-setting.svg | 1 +
.../public/assets/outline/radius-upleft.js | 7 +
.../public/assets/outline/radius-upleft.svg | 1 +
.../public/assets/outline/radius-upright.js | 7 +
.../public/assets/outline/radius-upright.svg | 1 +
.../src/public/assets/outline/read.js | 7 +
.../src/public/assets/outline/read.svg | 1 +
.../public/assets/outline/reconciliation.js | 7 +
.../public/assets/outline/reconciliation.svg | 1 +
.../src/public/assets/outline/red-envelope.js | 7 +
.../public/assets/outline/red-envelope.svg | 1 +
.../src/public/assets/outline/reddit.js | 7 +
.../src/public/assets/outline/reddit.svg | 1 +
.../src/public/assets/outline/redo.js | 7 +
.../src/public/assets/outline/redo.svg | 1 +
.../src/public/assets/outline/reload.js | 7 +
.../src/public/assets/outline/reload.svg | 1 +
.../src/public/assets/outline/rest.js | 7 +
.../src/public/assets/outline/rest.svg | 1 +
.../src/public/assets/outline/retweet.js | 7 +
.../src/public/assets/outline/retweet.svg | 1 +
.../src/public/assets/outline/right-circle.js | 7 +
.../public/assets/outline/right-circle.svg | 1 +
.../src/public/assets/outline/right-square.js | 7 +
.../public/assets/outline/right-square.svg | 1 +
.../src/public/assets/outline/right.js | 7 +
.../src/public/assets/outline/right.svg | 1 +
.../src/public/assets/outline/rise.js | 7 +
.../src/public/assets/outline/rise.svg | 1 +
.../src/public/assets/outline/robot.js | 7 +
.../src/public/assets/outline/robot.svg | 1 +
.../src/public/assets/outline/rocket.js | 7 +
.../src/public/assets/outline/rocket.svg | 1 +
.../src/public/assets/outline/rollback.js | 7 +
.../src/public/assets/outline/rollback.svg | 1 +
.../src/public/assets/outline/rotate-left.js | 7 +
.../src/public/assets/outline/rotate-left.svg | 1 +
.../src/public/assets/outline/rotate-right.js | 7 +
.../public/assets/outline/rotate-right.svg | 1 +
.../assets/outline/safety-certificate.js | 7 +
.../assets/outline/safety-certificate.svg | 1 +
.../src/public/assets/outline/safety.js | 7 +
.../src/public/assets/outline/safety.svg | 1 +
.../src/public/assets/outline/save.js | 7 +
.../src/public/assets/outline/save.svg | 1 +
.../src/public/assets/outline/scan.js | 7 +
.../src/public/assets/outline/scan.svg | 1 +
.../src/public/assets/outline/schedule.js | 7 +
.../src/public/assets/outline/schedule.svg | 1 +
.../src/public/assets/outline/scissor.js | 7 +
.../src/public/assets/outline/scissor.svg | 1 +
.../src/public/assets/outline/search.js | 7 +
.../src/public/assets/outline/search.svg | 1 +
.../public/assets/outline/security-scan.js | 7 +
.../public/assets/outline/security-scan.svg | 1 +
.../src/public/assets/outline/select.js | 7 +
.../src/public/assets/outline/select.svg | 1 +
.../src/public/assets/outline/send.js | 7 +
.../src/public/assets/outline/send.svg | 1 +
.../src/public/assets/outline/setting.js | 7 +
.../src/public/assets/outline/setting.svg | 1 +
.../src/public/assets/outline/shake.js | 7 +
.../src/public/assets/outline/shake.svg | 1 +
.../src/public/assets/outline/share-alt.js | 7 +
.../src/public/assets/outline/share-alt.svg | 1 +
.../src/public/assets/outline/shop.js | 7 +
.../src/public/assets/outline/shop.svg | 1 +
.../public/assets/outline/shopping-cart.js | 7 +
.../public/assets/outline/shopping-cart.svg | 1 +
.../src/public/assets/outline/shopping.js | 7 +
.../src/public/assets/outline/shopping.svg | 1 +
.../src/public/assets/outline/shrink.js | 7 +
.../src/public/assets/outline/shrink.svg | 1 +
.../src/public/assets/outline/sisternode.js | 7 +
.../src/public/assets/outline/sisternode.svg | 1 +
.../src/public/assets/outline/sketch.js | 7 +
.../src/public/assets/outline/sketch.svg | 1 +
.../src/public/assets/outline/skin.js | 7 +
.../src/public/assets/outline/skin.svg | 1 +
.../src/public/assets/outline/skype.js | 7 +
.../src/public/assets/outline/skype.svg | 1 +
.../src/public/assets/outline/slack-square.js | 7 +
.../public/assets/outline/slack-square.svg | 1 +
.../src/public/assets/outline/slack.js | 7 +
.../src/public/assets/outline/slack.svg | 1 +
.../src/public/assets/outline/sliders.js | 7 +
.../src/public/assets/outline/sliders.svg | 1 +
.../src/public/assets/outline/small-dash.js | 7 +
.../src/public/assets/outline/small-dash.svg | 1 +
.../src/public/assets/outline/smile.js | 7 +
.../src/public/assets/outline/smile.svg | 1 +
.../src/public/assets/outline/snippets.js | 7 +
.../src/public/assets/outline/snippets.svg | 1 +
.../src/public/assets/outline/solution.js | 7 +
.../src/public/assets/outline/solution.svg | 1 +
.../public/assets/outline/sort-ascending.js | 7 +
.../public/assets/outline/sort-ascending.svg | 1 +
.../public/assets/outline/sort-descending.js | 7 +
.../public/assets/outline/sort-descending.svg | 1 +
.../src/public/assets/outline/sound.js | 7 +
.../src/public/assets/outline/sound.svg | 1 +
.../src/public/assets/outline/split-cells.js | 7 +
.../src/public/assets/outline/split-cells.svg | 1 +
.../src/public/assets/outline/star.js | 7 +
.../src/public/assets/outline/star.svg | 1 +
.../public/assets/outline/step-backward.js | 7 +
.../public/assets/outline/step-backward.svg | 1 +
.../src/public/assets/outline/step-forward.js | 7 +
.../public/assets/outline/step-forward.svg | 1 +
.../src/public/assets/outline/stock.js | 7 +
.../src/public/assets/outline/stock.svg | 1 +
.../src/public/assets/outline/stop.js | 7 +
.../src/public/assets/outline/stop.svg | 1 +
.../public/assets/outline/strikethrough.js | 7 +
.../public/assets/outline/strikethrough.svg | 1 +
.../src/public/assets/outline/subnode.js | 7 +
.../src/public/assets/outline/subnode.svg | 1 +
.../src/public/assets/outline/swap-left.js | 7 +
.../src/public/assets/outline/swap-left.svg | 1 +
.../src/public/assets/outline/swap-right.js | 7 +
.../src/public/assets/outline/swap-right.svg | 1 +
.../src/public/assets/outline/swap.js | 7 +
.../src/public/assets/outline/swap.svg | 1 +
.../src/public/assets/outline/switcher.js | 7 +
.../src/public/assets/outline/switcher.svg | 1 +
.../src/public/assets/outline/sync.js | 7 +
.../src/public/assets/outline/sync.svg | 1 +
.../src/public/assets/outline/table.js | 7 +
.../src/public/assets/outline/table.svg | 1 +
.../src/public/assets/outline/tablet.js | 7 +
.../src/public/assets/outline/tablet.svg | 1 +
.../src/public/assets/outline/tag.js | 7 +
.../src/public/assets/outline/tag.svg | 1 +
.../src/public/assets/outline/tags.js | 7 +
.../src/public/assets/outline/tags.svg | 1 +
.../public/assets/outline/taobao-circle.js | 7 +
.../public/assets/outline/taobao-circle.svg | 1 +
.../src/public/assets/outline/taobao.js | 7 +
.../src/public/assets/outline/taobao.svg | 1 +
.../src/public/assets/outline/team.js | 7 +
.../src/public/assets/outline/team.svg | 1 +
.../src/public/assets/outline/thunderbolt.js | 7 +
.../src/public/assets/outline/thunderbolt.svg | 1 +
.../src/public/assets/outline/to-top.js | 7 +
.../src/public/assets/outline/to-top.svg | 1 +
.../src/public/assets/outline/tool.js | 7 +
.../src/public/assets/outline/tool.svg | 1 +
.../public/assets/outline/trademark-circle.js | 7 +
.../assets/outline/trademark-circle.svg | 1 +
.../src/public/assets/outline/trademark.js | 7 +
.../src/public/assets/outline/trademark.svg | 1 +
.../src/public/assets/outline/transaction.js | 7 +
.../src/public/assets/outline/transaction.svg | 1 +
.../src/public/assets/outline/translation.js | 7 +
.../src/public/assets/outline/translation.svg | 1 +
.../src/public/assets/outline/trophy.js | 7 +
.../src/public/assets/outline/trophy.svg | 1 +
.../src/public/assets/outline/twitter.js | 7 +
.../src/public/assets/outline/twitter.svg | 1 +
.../src/public/assets/outline/underline.js | 7 +
.../src/public/assets/outline/underline.svg | 1 +
.../src/public/assets/outline/undo.js | 7 +
.../src/public/assets/outline/undo.svg | 1 +
.../src/public/assets/outline/ungroup.js | 7 +
.../src/public/assets/outline/ungroup.svg | 1 +
.../src/public/assets/outline/unlock.js | 7 +
.../src/public/assets/outline/unlock.svg | 1 +
.../public/assets/outline/unordered-list.js | 7 +
.../public/assets/outline/unordered-list.svg | 1 +
.../src/public/assets/outline/up-circle.js | 7 +
.../src/public/assets/outline/up-circle.svg | 1 +
.../src/public/assets/outline/up-square.js | 7 +
.../src/public/assets/outline/up-square.svg | 1 +
.../src/public/assets/outline/up.js | 7 +
.../src/public/assets/outline/up.svg | 1 +
.../src/public/assets/outline/upload.js | 7 +
.../src/public/assets/outline/upload.svg | 1 +
.../src/public/assets/outline/usb.js | 7 +
.../src/public/assets/outline/usb.svg | 1 +
.../src/public/assets/outline/user-add.js | 7 +
.../src/public/assets/outline/user-add.svg | 1 +
.../src/public/assets/outline/user-delete.js | 7 +
.../src/public/assets/outline/user-delete.svg | 1 +
.../src/public/assets/outline/user-switch.js | 7 +
.../src/public/assets/outline/user-switch.svg | 1 +
.../src/public/assets/outline/user.js | 7 +
.../src/public/assets/outline/user.svg | 1 +
.../public/assets/outline/usergroup-add.js | 7 +
.../public/assets/outline/usergroup-add.svg | 1 +
.../public/assets/outline/usergroup-delete.js | 7 +
.../assets/outline/usergroup-delete.svg | 1 +
.../src/public/assets/outline/verified.js | 7 +
.../src/public/assets/outline/verified.svg | 1 +
.../assets/outline/vertical-align-bottom.js | 7 +
.../assets/outline/vertical-align-bottom.svg | 1 +
.../assets/outline/vertical-align-middle.js | 7 +
.../assets/outline/vertical-align-middle.svg | 1 +
.../assets/outline/vertical-align-top.js | 7 +
.../assets/outline/vertical-align-top.svg | 1 +
.../public/assets/outline/vertical-left.js | 7 +
.../public/assets/outline/vertical-left.svg | 1 +
.../public/assets/outline/vertical-right.js | 7 +
.../public/assets/outline/vertical-right.svg | 1 +
.../public/assets/outline/video-camera-add.js | 7 +
.../assets/outline/video-camera-add.svg | 1 +
.../src/public/assets/outline/video-camera.js | 7 +
.../public/assets/outline/video-camera.svg | 1 +
.../src/public/assets/outline/wallet.js | 7 +
.../src/public/assets/outline/wallet.svg | 1 +
.../src/public/assets/outline/warning.js | 7 +
.../src/public/assets/outline/warning.svg | 1 +
.../src/public/assets/outline/wechat.js | 7 +
.../src/public/assets/outline/wechat.svg | 1 +
.../src/public/assets/outline/weibo-circle.js | 7 +
.../public/assets/outline/weibo-circle.svg | 1 +
.../src/public/assets/outline/weibo-square.js | 7 +
.../public/assets/outline/weibo-square.svg | 1 +
.../src/public/assets/outline/weibo.js | 7 +
.../src/public/assets/outline/weibo.svg | 1 +
.../src/public/assets/outline/whats-app.js | 7 +
.../src/public/assets/outline/whats-app.svg | 1 +
.../src/public/assets/outline/wifi.js | 7 +
.../src/public/assets/outline/wifi.svg | 1 +
.../src/public/assets/outline/windows.js | 7 +
.../src/public/assets/outline/windows.svg | 1 +
.../src/public/assets/outline/woman.js | 7 +
.../src/public/assets/outline/woman.svg | 1 +
.../src/public/assets/outline/yahoo.js | 7 +
.../src/public/assets/outline/yahoo.svg | 1 +
.../src/public/assets/outline/youtube.js | 7 +
.../src/public/assets/outline/youtube.svg | 1 +
.../src/public/assets/outline/yuque.js | 7 +
.../src/public/assets/outline/yuque.svg | 1 +
.../src/public/assets/outline/zhihu.js | 7 +
.../src/public/assets/outline/zhihu.svg | 1 +
.../src/public/assets/outline/zoom-in.js | 7 +
.../src/public/assets/outline/zoom-in.svg | 1 +
.../src/public/assets/outline/zoom-out.js | 7 +
.../src/public/assets/outline/zoom-out.svg | 1 +
.../src/public/assets/twotone/.gitkeep | 0
.../src/public/assets/twotone/account-book.js | 7 +
.../public/assets/twotone/account-book.svg | 1 +
.../src/public/assets/twotone/alert.js | 7 +
.../src/public/assets/twotone/alert.svg | 1 +
.../src/public/assets/twotone/api.js | 7 +
.../src/public/assets/twotone/api.svg | 1 +
.../src/public/assets/twotone/appstore.js | 7 +
.../src/public/assets/twotone/appstore.svg | 1 +
.../src/public/assets/twotone/audio.js | 7 +
.../src/public/assets/twotone/audio.svg | 1 +
.../src/public/assets/twotone/bank.js | 7 +
.../src/public/assets/twotone/bank.svg | 1 +
.../src/public/assets/twotone/bell.js | 7 +
.../src/public/assets/twotone/bell.svg | 1 +
.../src/public/assets/twotone/book.js | 7 +
.../src/public/assets/twotone/book.svg | 1 +
.../src/public/assets/twotone/box-plot.js | 7 +
.../src/public/assets/twotone/box-plot.svg | 1 +
.../src/public/assets/twotone/bug.js | 7 +
.../src/public/assets/twotone/bug.svg | 1 +
.../src/public/assets/twotone/build.js | 7 +
.../src/public/assets/twotone/build.svg | 1 +
.../src/public/assets/twotone/bulb.js | 7 +
.../src/public/assets/twotone/bulb.svg | 1 +
.../src/public/assets/twotone/calculator.js | 7 +
.../src/public/assets/twotone/calculator.svg | 1 +
.../src/public/assets/twotone/calendar.js | 7 +
.../src/public/assets/twotone/calendar.svg | 1 +
.../src/public/assets/twotone/camera.js | 7 +
.../src/public/assets/twotone/camera.svg | 1 +
.../src/public/assets/twotone/car.js | 7 +
.../src/public/assets/twotone/car.svg | 1 +
.../src/public/assets/twotone/carry-out.js | 7 +
.../src/public/assets/twotone/carry-out.svg | 1 +
.../src/public/assets/twotone/check-circle.js | 7 +
.../public/assets/twotone/check-circle.svg | 1 +
.../src/public/assets/twotone/check-square.js | 7 +
.../public/assets/twotone/check-square.svg | 1 +
.../src/public/assets/twotone/ci-circle.js | 7 +
.../src/public/assets/twotone/ci-circle.svg | 1 +
.../src/public/assets/twotone/ci.js | 7 +
.../src/public/assets/twotone/ci.svg | 1 +
.../src/public/assets/twotone/clock-circle.js | 7 +
.../public/assets/twotone/clock-circle.svg | 1 +
.../src/public/assets/twotone/close-circle.js | 7 +
.../public/assets/twotone/close-circle.svg | 1 +
.../src/public/assets/twotone/close-square.js | 7 +
.../public/assets/twotone/close-square.svg | 1 +
.../src/public/assets/twotone/cloud.js | 7 +
.../src/public/assets/twotone/cloud.svg | 1 +
.../src/public/assets/twotone/code.js | 7 +
.../src/public/assets/twotone/code.svg | 1 +
.../src/public/assets/twotone/compass.js | 7 +
.../src/public/assets/twotone/compass.svg | 1 +
.../src/public/assets/twotone/contacts.js | 7 +
.../src/public/assets/twotone/contacts.svg | 1 +
.../src/public/assets/twotone/container.js | 7 +
.../src/public/assets/twotone/container.svg | 1 +
.../src/public/assets/twotone/control.js | 7 +
.../src/public/assets/twotone/control.svg | 1 +
.../src/public/assets/twotone/copy.js | 7 +
.../src/public/assets/twotone/copy.svg | 1 +
.../public/assets/twotone/copyright-circle.js | 7 +
.../assets/twotone/copyright-circle.svg | 1 +
.../src/public/assets/twotone/copyright.js | 7 +
.../src/public/assets/twotone/copyright.svg | 1 +
.../src/public/assets/twotone/credit-card.js | 7 +
.../src/public/assets/twotone/credit-card.svg | 1 +
.../src/public/assets/twotone/crown.js | 7 +
.../src/public/assets/twotone/crown.svg | 1 +
.../public/assets/twotone/customer-service.js | 7 +
.../assets/twotone/customer-service.svg | 1 +
.../src/public/assets/twotone/dashboard.js | 7 +
.../src/public/assets/twotone/dashboard.svg | 1 +
.../src/public/assets/twotone/database.js | 7 +
.../src/public/assets/twotone/database.svg | 1 +
.../src/public/assets/twotone/delete.js | 7 +
.../src/public/assets/twotone/delete.svg | 1 +
.../src/public/assets/twotone/diff.js | 7 +
.../src/public/assets/twotone/diff.svg | 1 +
.../src/public/assets/twotone/dislike.js | 7 +
.../src/public/assets/twotone/dislike.svg | 1 +
.../public/assets/twotone/dollar-circle.js | 7 +
.../public/assets/twotone/dollar-circle.svg | 1 +
.../src/public/assets/twotone/dollar.js | 7 +
.../src/public/assets/twotone/dollar.svg | 1 +
.../src/public/assets/twotone/down-circle.js | 7 +
.../src/public/assets/twotone/down-circle.svg | 1 +
.../src/public/assets/twotone/down-square.js | 7 +
.../src/public/assets/twotone/down-square.svg | 1 +
.../src/public/assets/twotone/edit.js | 7 +
.../src/public/assets/twotone/edit.svg | 1 +
.../src/public/assets/twotone/environment.js | 7 +
.../src/public/assets/twotone/environment.svg | 1 +
.../src/public/assets/twotone/euro-circle.js | 7 +
.../src/public/assets/twotone/euro-circle.svg | 1 +
.../src/public/assets/twotone/euro.js | 7 +
.../src/public/assets/twotone/euro.svg | 1 +
.../assets/twotone/exclamation-circle.js | 7 +
.../assets/twotone/exclamation-circle.svg | 1 +
.../src/public/assets/twotone/experiment.js | 7 +
.../src/public/assets/twotone/experiment.svg | 1 +
.../public/assets/twotone/eye-invisible.js | 7 +
.../public/assets/twotone/eye-invisible.svg | 1 +
.../src/public/assets/twotone/eye.js | 7 +
.../src/public/assets/twotone/eye.svg | 1 +
.../src/public/assets/twotone/file-add.js | 7 +
.../src/public/assets/twotone/file-add.svg | 1 +
.../src/public/assets/twotone/file-excel.js | 7 +
.../src/public/assets/twotone/file-excel.svg | 1 +
.../public/assets/twotone/file-exclamation.js | 7 +
.../assets/twotone/file-exclamation.svg | 1 +
.../src/public/assets/twotone/file-image.js | 7 +
.../src/public/assets/twotone/file-image.svg | 1 +
.../public/assets/twotone/file-markdown.js | 7 +
.../public/assets/twotone/file-markdown.svg | 1 +
.../src/public/assets/twotone/file-pdf.js | 7 +
.../src/public/assets/twotone/file-pdf.svg | 1 +
.../src/public/assets/twotone/file-ppt.js | 7 +
.../src/public/assets/twotone/file-ppt.svg | 1 +
.../src/public/assets/twotone/file-text.js | 7 +
.../src/public/assets/twotone/file-text.svg | 1 +
.../src/public/assets/twotone/file-unknown.js | 7 +
.../public/assets/twotone/file-unknown.svg | 1 +
.../src/public/assets/twotone/file-word.js | 7 +
.../src/public/assets/twotone/file-word.svg | 1 +
.../src/public/assets/twotone/file-zip.js | 7 +
.../src/public/assets/twotone/file-zip.svg | 1 +
.../src/public/assets/twotone/file.js | 7 +
.../src/public/assets/twotone/file.svg | 1 +
.../src/public/assets/twotone/filter.js | 7 +
.../src/public/assets/twotone/filter.svg | 1 +
.../src/public/assets/twotone/fire.js | 7 +
.../src/public/assets/twotone/fire.svg | 1 +
.../src/public/assets/twotone/flag.js | 7 +
.../src/public/assets/twotone/flag.svg | 1 +
.../src/public/assets/twotone/folder-add.js | 7 +
.../src/public/assets/twotone/folder-add.svg | 1 +
.../src/public/assets/twotone/folder-open.js | 7 +
.../src/public/assets/twotone/folder-open.svg | 1 +
.../src/public/assets/twotone/folder.js | 7 +
.../src/public/assets/twotone/folder.svg | 1 +
.../src/public/assets/twotone/frown.js | 7 +
.../src/public/assets/twotone/frown.svg | 1 +
.../src/public/assets/twotone/fund.js | 7 +
.../src/public/assets/twotone/fund.svg | 1 +
.../src/public/assets/twotone/funnel-plot.js | 7 +
.../src/public/assets/twotone/funnel-plot.svg | 1 +
.../src/public/assets/twotone/gift.js | 7 +
.../src/public/assets/twotone/gift.svg | 1 +
.../src/public/assets/twotone/gold.js | 7 +
.../src/public/assets/twotone/gold.svg | 1 +
.../src/public/assets/twotone/hdd.js | 7 +
.../src/public/assets/twotone/hdd.svg | 1 +
.../src/public/assets/twotone/heart.js | 7 +
.../src/public/assets/twotone/heart.svg | 1 +
.../src/public/assets/twotone/highlight.js | 7 +
.../src/public/assets/twotone/highlight.svg | 1 +
.../src/public/assets/twotone/home.js | 7 +
.../src/public/assets/twotone/home.svg | 1 +
.../src/public/assets/twotone/hourglass.js | 7 +
.../src/public/assets/twotone/hourglass.svg | 1 +
.../src/public/assets/twotone/html5.js | 7 +
.../src/public/assets/twotone/html5.svg | 1 +
.../src/public/assets/twotone/idcard.js | 7 +
.../src/public/assets/twotone/idcard.svg | 1 +
.../src/public/assets/twotone/info-circle.js | 7 +
.../src/public/assets/twotone/info-circle.svg | 1 +
.../src/public/assets/twotone/insurance.js | 7 +
.../src/public/assets/twotone/insurance.svg | 1 +
.../src/public/assets/twotone/interaction.js | 7 +
.../src/public/assets/twotone/interaction.svg | 1 +
.../src/public/assets/twotone/layout.js | 7 +
.../src/public/assets/twotone/layout.svg | 1 +
.../src/public/assets/twotone/left-circle.js | 7 +
.../src/public/assets/twotone/left-circle.svg | 1 +
.../src/public/assets/twotone/left-square.js | 7 +
.../src/public/assets/twotone/left-square.svg | 1 +
.../src/public/assets/twotone/like.js | 7 +
.../src/public/assets/twotone/like.svg | 1 +
.../src/public/assets/twotone/lock.js | 7 +
.../src/public/assets/twotone/lock.svg | 1 +
.../src/public/assets/twotone/mail.js | 7 +
.../src/public/assets/twotone/mail.svg | 1 +
.../src/public/assets/twotone/medicine-box.js | 7 +
.../public/assets/twotone/medicine-box.svg | 1 +
.../src/public/assets/twotone/meh.js | 7 +
.../src/public/assets/twotone/meh.svg | 1 +
.../src/public/assets/twotone/message.js | 7 +
.../src/public/assets/twotone/message.svg | 1 +
.../src/public/assets/twotone/minus-circle.js | 7 +
.../public/assets/twotone/minus-circle.svg | 1 +
.../src/public/assets/twotone/minus-square.js | 7 +
.../public/assets/twotone/minus-square.svg | 1 +
.../src/public/assets/twotone/mobile.js | 7 +
.../src/public/assets/twotone/mobile.svg | 1 +
.../public/assets/twotone/money-collect.js | 7 +
.../public/assets/twotone/money-collect.svg | 1 +
.../src/public/assets/twotone/notification.js | 7 +
.../public/assets/twotone/notification.svg | 1 +
.../src/public/assets/twotone/pause-circle.js | 7 +
.../public/assets/twotone/pause-circle.svg | 1 +
.../src/public/assets/twotone/phone.js | 7 +
.../src/public/assets/twotone/phone.svg | 1 +
.../src/public/assets/twotone/picture.js | 7 +
.../src/public/assets/twotone/picture.svg | 1 +
.../src/public/assets/twotone/pie-chart.js | 7 +
.../src/public/assets/twotone/pie-chart.svg | 1 +
.../src/public/assets/twotone/play-circle.js | 7 +
.../src/public/assets/twotone/play-circle.svg | 1 +
.../src/public/assets/twotone/play-square.js | 7 +
.../src/public/assets/twotone/play-square.svg | 1 +
.../src/public/assets/twotone/plus-circle.js | 7 +
.../src/public/assets/twotone/plus-circle.svg | 1 +
.../src/public/assets/twotone/plus-square.js | 7 +
.../src/public/assets/twotone/plus-square.svg | 1 +
.../src/public/assets/twotone/pound-circle.js | 7 +
.../public/assets/twotone/pound-circle.svg | 1 +
.../src/public/assets/twotone/printer.js | 7 +
.../src/public/assets/twotone/printer.svg | 1 +
.../src/public/assets/twotone/profile.js | 7 +
.../src/public/assets/twotone/profile.svg | 1 +
.../src/public/assets/twotone/project.js | 7 +
.../src/public/assets/twotone/project.svg | 1 +
.../public/assets/twotone/property-safety.js | 7 +
.../public/assets/twotone/property-safety.svg | 1 +
.../src/public/assets/twotone/pushpin.js | 7 +
.../src/public/assets/twotone/pushpin.svg | 1 +
.../public/assets/twotone/question-circle.js | 7 +
.../public/assets/twotone/question-circle.svg | 1 +
.../public/assets/twotone/reconciliation.js | 7 +
.../public/assets/twotone/reconciliation.svg | 1 +
.../src/public/assets/twotone/red-envelope.js | 7 +
.../public/assets/twotone/red-envelope.svg | 1 +
.../src/public/assets/twotone/rest.js | 7 +
.../src/public/assets/twotone/rest.svg | 1 +
.../src/public/assets/twotone/right-circle.js | 7 +
.../public/assets/twotone/right-circle.svg | 1 +
.../src/public/assets/twotone/right-square.js | 7 +
.../public/assets/twotone/right-square.svg | 1 +
.../src/public/assets/twotone/rocket.js | 7 +
.../src/public/assets/twotone/rocket.svg | 1 +
.../assets/twotone/safety-certificate.js | 7 +
.../assets/twotone/safety-certificate.svg | 1 +
.../src/public/assets/twotone/save.js | 7 +
.../src/public/assets/twotone/save.svg | 1 +
.../src/public/assets/twotone/schedule.js | 7 +
.../src/public/assets/twotone/schedule.svg | 1 +
.../public/assets/twotone/security-scan.js | 7 +
.../public/assets/twotone/security-scan.svg | 1 +
.../src/public/assets/twotone/setting.js | 7 +
.../src/public/assets/twotone/setting.svg | 1 +
.../src/public/assets/twotone/shop.js | 7 +
.../src/public/assets/twotone/shop.svg | 1 +
.../src/public/assets/twotone/shopping.js | 7 +
.../src/public/assets/twotone/shopping.svg | 1 +
.../src/public/assets/twotone/skin.js | 7 +
.../src/public/assets/twotone/skin.svg | 1 +
.../src/public/assets/twotone/sliders.js | 7 +
.../src/public/assets/twotone/sliders.svg | 1 +
.../src/public/assets/twotone/smile.js | 7 +
.../src/public/assets/twotone/smile.svg | 1 +
.../src/public/assets/twotone/snippets.js | 7 +
.../src/public/assets/twotone/snippets.svg | 1 +
.../src/public/assets/twotone/sound.js | 7 +
.../src/public/assets/twotone/sound.svg | 1 +
.../src/public/assets/twotone/star.js | 7 +
.../src/public/assets/twotone/star.svg | 1 +
.../src/public/assets/twotone/stop.js | 7 +
.../src/public/assets/twotone/stop.svg | 1 +
.../src/public/assets/twotone/switcher.js | 7 +
.../src/public/assets/twotone/switcher.svg | 1 +
.../src/public/assets/twotone/tablet.js | 7 +
.../src/public/assets/twotone/tablet.svg | 1 +
.../src/public/assets/twotone/tag.js | 7 +
.../src/public/assets/twotone/tag.svg | 1 +
.../src/public/assets/twotone/tags.js | 7 +
.../src/public/assets/twotone/tags.svg | 1 +
.../src/public/assets/twotone/thunderbolt.js | 7 +
.../src/public/assets/twotone/thunderbolt.svg | 1 +
.../src/public/assets/twotone/tool.js | 7 +
.../src/public/assets/twotone/tool.svg | 1 +
.../public/assets/twotone/trademark-circle.js | 7 +
.../assets/twotone/trademark-circle.svg | 1 +
.../src/public/assets/twotone/trophy.js | 7 +
.../src/public/assets/twotone/trophy.svg | 1 +
.../src/public/assets/twotone/unlock.js | 7 +
.../src/public/assets/twotone/unlock.svg | 1 +
.../src/public/assets/twotone/up-circle.js | 7 +
.../src/public/assets/twotone/up-circle.svg | 1 +
.../src/public/assets/twotone/up-square.js | 7 +
.../src/public/assets/twotone/up-square.svg | 1 +
.../src/public/assets/twotone/usb.js | 7 +
.../src/public/assets/twotone/usb.svg | 1 +
.../src/public/assets/twotone/video-camera.js | 7 +
.../public/assets/twotone/video-camera.svg | 1 +
.../src/public/assets/twotone/wallet.js | 7 +
.../src/public/assets/twotone/wallet.svg | 1 +
.../src/public/assets/twotone/warning.js | 7 +
.../src/public/assets/twotone/warning.svg | 1 +
worklenz-backend/src/public/favicon.ico | Bin 0 -> 11810 bytes
.../src/public/main.91ef34cb24678df1.js | 1 +
.../src/public/manifest.webmanifest | 59 +
worklenz-backend/src/public/ngsw-worker.js | 1852 ++
worklenz-backend/src/public/ngsw.json | 3419 +++
.../src/public/polyfills.f07f6ddac0a4feb2.js | 1 +
.../src/public/runtime.9997dc39514a207d.js | 1 +
worklenz-backend/src/public/safety-worker.js | 26 +
.../src/public/styles.c78b93a1a5b19d7f.css | 9 +
.../src/public/worker-basic.min.js | 26 +
worklenz-backend/src/redis/client.ts | 11 +
.../routes/apis/activity-logs-api-router.ts | 11 +
.../routes/apis/admin-center-api-router.ts | 30 +
.../src/routes/apis/attachments-api-router.ts | 20 +
.../src/routes/apis/clients-api-router.ts | 19 +
.../apis/gannt-apis/roadmap-api-router.ts | 18 +
.../apis/gannt-apis/schedule-api-router.ts | 23 +
.../apis/gannt-apis/workload-api-router.ts | 19 +
.../src/routes/apis/gantt-api-router.ts | 15 +
.../src/routes/apis/home-page-api-router.ts | 15 +
worklenz-backend/src/routes/apis/index.ts | 105 +
.../src/routes/apis/job-titles-api-router.ts | 17 +
.../src/routes/apis/labels-api-router.ts | 16 +
.../routes/apis/notifications-api-router.ts | 14 +
.../apis/personal-overview-api-router.ts | 12 +
.../src/routes/apis/priorities-api-router.ts | 10 +
.../apis/project-categories-api-router.ts | 15 +
.../apis/project-comments-api-router.ts | 15 +
.../routes/apis/project-folders-api-router.ts | 17 +
.../routes/apis/project-healths-api-router.ts | 9 +
.../apis/project-insights-api-router.ts | 26 +
.../apis/project-managers-api-router.ts | 9 +
.../routes/apis/project-members-api-router.ts | 18 +
.../apis/project-statuses-api-router.ts | 10 +
.../src/routes/apis/project-templates-api.ts | 24 +
.../src/routes/apis/projects-api-router.ts | 37 +
.../src/routes/apis/pt-statuses-api-router.ts | 19 +
.../src/routes/apis/pt-tasks-api-router.ts | 20 +
.../routes/apis/pt_task-phases-api-router.ts | 19 +
.../src/routes/apis/reporting-api-router.ts | 73 +
.../apis/reporting-export-api-router.ts | 24 +
.../apis/resource-allocation-api-router.ts | 11 +
.../src/routes/apis/settings-api-router.ts | 24 +
.../routes/apis/shared-projects-api-router.ts | 14 +
.../src/routes/apis/statuses-api-router.ts | 24 +
.../src/routes/apis/sub-tasks-api-router.ts | 14 +
.../routes/apis/task-comments-api-router.ts | 15 +
.../src/routes/apis/task-phases-api-router.ts | 20 +
.../routes/apis/task-templates-api-router.ts | 18 +
.../routes/apis/task-work-log-api-router.ts | 17 +
.../src/routes/apis/tasks-api-router.ts | 66 +
.../routes/apis/team-members-api-router.ts | 35 +
.../src/routes/apis/teams-api-router.ts | 17 +
.../src/routes/apis/timezones-api-router.ts | 11 +
.../src/routes/apis/todo-list-api-router.ts | 18 +
worklenz-backend/src/routes/auth/index.ts | 59 +
.../src/routes/email-templates.ts | 154 +
worklenz-backend/src/routes/index.ts | 20 +
worklenz-backend/src/routes/public/index.ts | 9 +
.../activity-logs/activity-logs.service.ts | 206 +
.../src/services/activity-logs/interfaces.ts | 40 +
.../email-notifications.service.ts | 5 +
.../src/services/notifications/interfaces.ts | 24 +
.../notifications/messages/TaskAdd.ts | 15 +
.../notifications/messages/TaskRemove.ts | 15 +
.../notifications/notification-types.ts | 14 +
.../services/notifications/notification.ts | 42 +
.../notifications/notifications.service.ts | 118 +
worklenz-backend/src/shared/constants.ts | 133 +
worklenz-backend/src/shared/constraints.ts | 69 +
worklenz-backend/src/shared/csp.ts | 56 +
.../src/shared/email-notifications.ts | 122 +
.../src/shared/email-templates.ts | 108 +
worklenz-backend/src/shared/email.ts | 92 +
worklenz-backend/src/shared/file-constants.ts | 95 +
worklenz-backend/src/shared/io.ts | 48 +
.../src/shared/password-strength-check.ts | 78 +
.../src/shared/postgresql-error-codes.json | 242 +
worklenz-backend/src/shared/s3.ts | 135 +
.../src/shared/safe-controller-function.ts | 10 +
worklenz-backend/src/shared/slack.ts | 35 +
.../src/shared/tasks-controller-utils.ts | 147 +
worklenz-backend/src/shared/utils.ts | 217 +
.../src/socket.io/commands/.gitkeep | 0
.../src/socket.io/commands/on-connect.ts | 16 +
.../src/socket.io/commands/on-create-label.ts | 62 +
.../commands/on-create-project-category.ts | 30 +
.../src/socket.io/commands/on-disconnect.ts | 14 +
.../commands/on-get-task-progress.ts | 24 +
.../commands/on-join-or-leave-project-room.ts | 49 +
.../commands/on-phase-end-date-change.ts | 28 +
.../commands/on-phase-start-date-change.ts | 28 +
.../commands/on-project-category-change.ts | 37 +
.../commands/on-project-end-date-change.ts | 21 +
.../commands/on-project-health-change.ts | 26 +
.../commands/on-project-start-date-change.ts | 21 +
.../commands/on-project-status-change.ts | 28 +
.../commands/on-project-subscriber-change.ts | 37 +
.../commands/on-quick-assign-or-remove.ts | 124 +
.../src/socket.io/commands/on-quick-task.ts | 120 +
.../commands/on-roadmap-sort-order-change.ts | 21 +
.../commands/on-task-description-change.ts | 39 +
.../commands/on-task-end-date-change.ts | 39 +
.../commands/on-task-labels-change.ts | 34 +
.../socket.io/commands/on-task-name-change.ts | 52 +
.../commands/on-task-phase-change.ts | 47 +
.../commands/on-task-priority-change.ts | 41 +
.../commands/on-task-sort-order-change.ts | 134 +
.../commands/on-task-start-date-change.ts | 43 +
.../commands/on-task-status-change.ts | 77 +
.../commands/on-task-subscriber-change.ts | 37 +
.../socket.io/commands/on-task-timer-start.ts | 33 +
.../socket.io/commands/on-task-timer-stop.ts | 48 +
.../commands/on-time-estimation-change.ts | 45 +
.../commands/on_gannt_drag_change.ts | 54 +
.../socket.io/commands/on_pt_create_label.ts | 51 +
.../socket.io/commands/on_pt_name_change.ts | 24 +
.../socket.io/commands/on_pt_quick_task.ts | 51 +
.../commands/on_pt_task_description_change.ts | 24 +
.../commands/on_pt_task_end_date_change.ts | 24 +
.../commands/on_pt_task_labels_change.ts | 26 +
.../commands/on_pt_task_name_change.ts | 25 +
.../commands/on_pt_task_phase_change.ts | 33 +
.../commands/on_pt_task_priority_change.ts | 31 +
.../commands/on_pt_task_sort_order_change.ts | 84 +
.../commands/on_pt_task_start_date_change.ts | 24 +
.../commands/on_pt_task_status_change.ts | 28 +
.../on_pt_task_time_estimation_change.ts | 27 +
.../on_schedule_member_allocation_create.ts | 66 +
.../on_schedule_member_end_date_change.ts | 36 +
.../on_schedule_member_start_date_change.ts | 36 +
worklenz-backend/src/socket.io/events.ts | 55 +
worklenz-backend/src/socket.io/index.ts | 102 +
worklenz-backend/src/socket.io/util.ts | 31 +
worklenz-backend/src/tests/.gitkeep | 0
worklenz-backend/src/tests/constants.spec.ts | 15 +
worklenz-backend/src/tests/test.spec.ts | 3 +
worklenz-backend/src/typings.d.ts | 35 +
.../src/utils/generate-project-key.ts | 61 +
worklenz-backend/src/utils/logger.ts | 36 +
worklenz-backend/src/views/_footer.pug | 30 +
worklenz-backend/src/views/_header.pug | 38 +
worklenz-backend/src/views/_hubspot.pug | 1 +
worklenz-backend/src/views/_scripts.pug | 0
worklenz-backend/src/views/admin/_fonts.pug | 567 +
.../src/views/admin/_maintenance.pug | 10 +
worklenz-backend/src/views/admin/_styles.pug | 158 +
worklenz-backend/src/views/admin/index.pug | 4 +
worklenz-backend/src/views/admin/layout.pug | 39 +
worklenz-backend/src/views/error/index.pug | 15 +
worklenz-backend/src/views/error/layout.pug | 25 +
worklenz-backend/src/views/index.pug | 302 +
worklenz-backend/src/views/layout.pug | 53 +
worklenz-backend/src/views/privacy-policy.pug | 160 +
worklenz-backend/src/views/terms-of-use.pug | 143 +
worklenz-backend/tsconfig.json | 100 +
worklenz-backend/tsconfig.prod.json | 100 +
.../admin-new-subscriber-notification.html | 234 +
.../deprecation-notice.html | 246 +
.../email-notifications/daily-digest.pug | 106 +
.../project-daily-digest.pug | 111 +
.../task-assignee-change.pug | 120 +
.../email-notifications/task-comment.pug | 82 +
.../task-moved-to-done.pug | 85 +
.../maintenance-template.html | 255 +
.../otp-verfication-code.html | 231 +
.../password-changed-notification.html | 210 +
.../reset-password.html | 254 +
.../team-invitation.html | 256 +
...gistered-team-invitation-notification.html | 255 +
.../worklenz-email-templates/welcome.html | 254 +
worklenz-frontend/.editorconfig | 16 +
worklenz-frontend/.eslintrc.json | 46 +
worklenz-frontend/.gitignore | 44 +
worklenz-frontend/.npmrc | 2 +
worklenz-frontend/Dockerfile | 11 +
worklenz-frontend/README.md | 37 +
worklenz-frontend/angular.json | 150 +
worklenz-frontend/karma.conf.js | 44 +
worklenz-frontend/ngsw-config.json | 30 +
worklenz-frontend/package-lock.json | 16478 ++++++++++++
worklenz-frontend/package.json | 76 +
worklenz-frontend/proxy.config.json | 24 +
worklenz-frontend/src/app/DTOs/.gitkeep | 0
.../account-setup-routing.module.ts | 16 +
.../account-setup/account-setup.module.ts | 49 +
.../account-setup.component.html | 161 +
.../account-setup.component.scss | 116 +
.../account-setup.component.spec.ts | 23 +
.../account-setup/account-setup.component.ts | 298 +
.../teams-list/teams-list.component.html | 50 +
.../teams-list/teams-list.component.scss | 16 +
.../teams-list/teams-list.component.spec.ts | 21 +
.../teams-list/teams-list.component.ts | 132 +
.../admin-center-routing.module.ts | 28 +
.../admin-center-service.service.ts | 27 +
.../admin-center/admin-center.module.ts | 86 +
.../admin-center/layout/layout.component.html | 13 +
.../admin-center/layout/layout.component.scss | 3 +
.../layout/layout.component.spec.ts | 23 +
.../admin-center/layout/layout.component.ts | 10 +
.../overview/overview.component.html | 75 +
.../overview/overview.component.scss | 42 +
.../overview/overview.component.spec.ts | 23 +
.../overview/overview.component.ts | 104 +
.../sidebar/sidebar.component.html | 11 +
.../sidebar/sidebar.component.scss | 0
.../sidebar/sidebar.component.spec.ts | 23 +
.../admin-center/sidebar/sidebar.component.ts | 18 +
.../admin-center/teams/teams.component.html | 260 +
.../admin-center/teams/teams.component.scss | 11 +
.../teams/teams.component.spec.ts | 23 +
.../admin-center/teams/teams.component.ts | 293 +
.../admin-center/users/users.component.html | 75 +
.../admin-center/users/users.component.scss | 45 +
.../users/users.component.spec.ts | 23 +
.../admin-center/users/users.component.ts | 75 +
.../administrator-routing.module.ts | 55 +
.../app/administrator/administrator.module.ts | 91 +
.../components/avatars/avatars.component.html | 29 +
.../components/avatars/avatars.component.scss | 0
.../avatars/avatars.component.spec.ts | 23 +
.../components/avatars/avatars.component.ts | 31 +
.../clients-autocomplete.component.html | 21 +
.../clients-autocomplete.component.scss | 0
.../clients-autocomplete.component.spec.ts | 23 +
.../clients-autocomplete.component.ts | 88 +
.../convert-to-subtask-modal.component.html | 50 +
.../convert-to-subtask-modal.component.scss | 86 +
...convert-to-subtask-modal.component.spec.ts | 25 +
.../convert-to-subtask-modal.component.ts | 229 +
.../import-tasks-template.component.html | 48 +
.../import-tasks-template.component.scss | 0
.../import-tasks-template.component.spec.ts | 23 +
.../import-tasks-template.component.ts | 190 +
.../job-titles-autocomplete.component.html | 22 +
.../job-titles-autocomplete.component.scss | 0
.../job-titles-autocomplete.component.spec.ts | 23 +
.../job-titles-autocomplete.component.ts | 100 +
.../components/na/na.component.html | 1 +
.../components/na/na.component.scss | 0
.../components/na/na.component.spec.ts | 21 +
.../components/na/na.component.ts | 15 +
.../personal-overview.component.html | 53 +
.../personal-overview.component.scss | 0
.../personal-overview.component.spec.ts | 23 +
.../personal-overview.component.ts | 93 +
...ect-categories-autocomplete.component.html | 32 +
...ect-categories-autocomplete.component.scss | 0
...-categories-autocomplete.component.spec.ts | 21 +
...oject-categories-autocomplete.component.ts | 141 +
...roject-folders-autocomplete.component.html | 21 +
...roject-folders-autocomplete.component.scss | 0
...ect-folders-autocomplete.component.spec.ts | 21 +
.../project-folders-autocomplete.component.ts | 91 +
.../project-form-modal.component.html | 280 +
.../project-form-modal.component.scss | 36 +
.../project-form-modal.component.spec.ts | 23 +
.../project-form-modal.component.ts | 594 +
.../project-members-form.component.html | 58 +
.../project-members-form.component.scss | 0
.../project-members-form.component.spec.ts | 23 +
.../project-members-form.component.ts | 162 +
...ject-template-create-drawer.component.html | 95 +
...ject-template-create-drawer.component.scss | 18 +
...t-template-create-drawer.component.spec.ts | 21 +
...roject-template-create-drawer.component.ts | 143 +
.../custom-template-list.component.html | 28 +
.../custom-template-list.component.scss | 31 +
.../custom-template-list.component.spec.ts | 21 +
.../custom-template-list.component.ts | 74 +
...ject-template-import-drawer.component.html | 37 +
...ject-template-import-drawer.component.scss | 0
...t-template-import-drawer.component.spec.ts | 21 +
...roject-template-import-drawer.component.ts | 138 +
.../worklenz-template-list.component.html | 93 +
.../worklenz-template-list.component.scss | 33 +
.../worklenz-template-list.component.spec.ts | 21 +
.../worklenz-template-list.component.ts | 84 +
.../project-updates-drawer.component.html | 14 +
.../project-updates-drawer.component.scss | 24 +
.../project-updates-drawer.component.spec.ts | 21 +
.../project-updates-drawer.component.ts | 73 +
.../project-updates-input.component.html | 1 +
.../project-updates-input.component.scss | 1 +
.../project-updates-input.component.spec.ts | 21 +
.../project-updates-input.component.ts | 31 +
.../project-updates-list.component.html | 51 +
.../project-updates-list.component.scss | 10 +
.../project-updates-list.component.spec.ts | 21 +
.../project-updates-list.component.ts | 270 +
.../projects-autocomplete.component.html | 26 +
.../projects-autocomplete.component.scss | 0
.../projects-autocomplete.component.spec.ts | 23 +
.../projects-autocomplete.component.ts | 95 +
.../resource-gantt.component.html | 54 +
.../resource-gantt.component.scss | 110 +
.../resource-gantt.component.spec.ts | 23 +
.../resource-gantt.component.ts | 119 +
.../status-form/status-form.component.html | 55 +
.../status-form/status-form.component.scss | 9 +
.../status-form/status-form.component.spec.ts | 23 +
.../status-form/status-form.component.ts | 199 +
.../task-priority-label.component.html | 8 +
.../task-priority-label.component.scss | 18 +
.../task-priority-label.component.spec.ts | 23 +
.../task-priority-label.component.ts | 15 +
.../task-template-drawer.component.html | 39 +
.../task-template-drawer.component.scss | 0
.../task-template-drawer.component.spec.ts | 23 +
.../task-template-drawer.component.ts | 158 +
.../components/task-timer/interfaces.ts | 4 +
.../task-timer/task-timer.component.html | 18 +
.../task-timer/task-timer.component.scss | 16 +
.../task-timer/task-timer.component.spec.ts | 23 +
.../task-timer/task-timer.component.ts | 193 +
.../task-timer/task-timer.service.ts | 52 +
.../components/task-view/interfaces.ts | 15 +
.../task-view-activity-log.component.html | 101 +
.../task-view-activity-log.component.scss | 0
.../task-view-activity-log.component.spec.ts | 21 +
.../task-view-activity-log.component.ts | 69 +
.../task-view-assignees.component.html | 58 +
.../task-view-assignees.component.scss | 17 +
.../task-view-assignees.component.spec.ts | 23 +
.../task-view-assignees.component.ts | 135 +
...task-view-attachments-thumb.component.html | 113 +
...task-view-attachments-thumb.component.scss | 45 +
...k-view-attachments-thumb.component.spec.ts | 23 +
.../task-view-attachments-thumb.component.ts | 136 +
.../task-view-attachments.component.html | 28 +
.../task-view-attachments.component.scss | 11 +
.../task-view-attachments.component.spec.ts | 23 +
.../task-view-attachments.component.ts | 163 +
.../task-view-comments-input.component.html | 32 +
.../task-view-comments-input.component.scss | 10 +
...task-view-comments-input.component.spec.ts | 23 +
.../task-view-comments-input.component.ts | 179 +
.../task-view-comments.component.html | 21 +
.../task-view-comments.component.scss | 13 +
.../task-view-comments.component.spec.ts | 23 +
.../task-view-comments.component.ts | 102 +
.../task-view-description.component.html | 20 +
.../task-view-description.component.scss | 36 +
.../task-view-description.component.spec.ts | 23 +
.../task-view-description.component.ts | 97 +
.../task-view-due-date.component.html | 41 +
.../task-view-due-date.component.scss | 28 +
.../task-view-due-date.component.spec.ts | 23 +
.../task-view-due-date.component.ts | 91 +
.../task-view-estimation.component.html | 24 +
.../task-view-estimation.component.scss | 0
.../task-view-estimation.component.spec.ts | 23 +
.../task-view-estimation.component.ts | 32 +
.../task-view-info.component.html | 72 +
.../task-view-info.component.scss | 19 +
.../task-view-info.component.spec.ts | 23 +
.../task-view-info.component.ts | 53 +
.../task-view-labels.component.html | 39 +
.../task-view-labels.component.scss | 3 +
.../task-view-labels.component.spec.ts | 23 +
.../task-view-labels.component.ts | 137 +
.../task-view-name.component.html | 46 +
.../task-view-name.component.scss | 52 +
.../task-view-name.component.spec.ts | 23 +
.../task-view-name.component.ts | 141 +
.../task-view-notify-to-user.component.html | 56 +
.../task-view-notify-to-user.component.scss | 17 +
...task-view-notify-to-user.component.spec.ts | 21 +
.../task-view-notify-to-user.component.ts | 154 +
.../task-view-phase.component.html | 22 +
.../task-view-phase.component.scss | 0
.../task-view-phase.component.spec.ts | 21 +
.../task-view-phase.component.ts | 55 +
.../task-view-priority.component.html | 23 +
.../task-view-priority.component.scss | 0
.../task-view-priority.component.spec.ts | 23 +
.../task-view-priority.component.ts | 62 +
.../task-view-sub-tasks.component.html | 90 +
.../task-view-sub-tasks.component.scss | 4 +
.../task-view-sub-tasks.component.spec.ts | 23 +
.../task-view-sub-tasks.component.ts | 216 +
.../task-view-time-log.component.html | 164 +
.../task-view-time-log.component.scss | 73 +
.../task-view-time-log.component.spec.ts | 23 +
.../task-view-time-log.component.ts | 452 +
.../task-view/task-view.component.html | 67 +
.../task-view/task-view.component.scss | 0
.../task-view/task-view.component.spec.ts | 23 +
.../task-view/task-view.component.ts | 334 +
.../components/task-view/task-view.module.ts | 158 +
.../task-view/task-view.service.spec.ts | 16 +
.../components/task-view/task-view.service.ts | 170 +
.../tasks-group-view.component.html | 16 +
.../tasks-group-view.component.scss | 49 +
.../tasks-group-view.component.spec.ts | 23 +
.../tasks-group-view.component.ts | 141 +
.../tasks-progress-bar.component.html | 25 +
.../tasks-progress-bar.component.scss | 51 +
.../tasks-progress-bar.component.spec.ts | 21 +
.../tasks-progress-bar.component.ts | 35 +
.../with-percentage-mark.pipe.ts | 11 +
.../team-members-autocomplete.component.html | 58 +
.../team-members-autocomplete.component.scss | 0
...eam-members-autocomplete.component.spec.ts | 23 +
.../team-members-autocomplete.component.ts | 211 +
.../team-members-form.component.html | 99 +
.../team-members-form.component.scss | 0
.../team-members-form.component.spec.ts | 23 +
.../team-members-form.component.ts | 248 +
.../toggle-menu-button.component.html | 9 +
.../toggle-menu-button.component.scss | 0
.../toggle-menu-button.component.spec.ts | 23 +
.../toggle-menu-button.component.ts | 25 +
.../layout/alerts/alerts.component.html | 22 +
.../layout/alerts/alerts.component.scss | 0
.../layout/alerts/alerts.component.spec.ts | 23 +
.../layout/alerts/alerts.component.ts | 21 +
.../layout/header/header.component.html | 194 +
.../layout/header/header.component.scss | 207 +
.../layout/header/header.component.spec.ts | 23 +
.../layout/header/header.component.ts | 178 +
.../layout/layout.component.html | 37 +
.../layout/layout.component.scss | 37 +
.../layout/layout.component.spec.ts | 25 +
.../administrator/layout/layout.component.ts | 217 +
.../licensing-alerts.component.html | 20 +
.../licensing-alerts.component.scss | 0
.../licensing-alerts.component.spec.ts | 21 +
.../licensing-alerts.component.ts | 68 +
.../notification-template.component.html | 27 +
.../notification-template.component.scss | 0
.../notification-template.component.spec.ts | 21 +
.../notification-template.component.ts | 75 +
.../notifications-drawer.component.html | 96 +
.../notifications-drawer.component.scss | 3 +
.../notifications-drawer.component.spec.ts | 23 +
.../notifications-drawer.component.ts | 433 +
.../tag-background.pipe.ts | 13 +
.../layout/notifications-drawer/types.ts | 7 +
.../task-card/task-card.component.html | 43 +
.../task-card/task-card.component.scss | 0
.../task-card/task-card.component.spec.ts | 21 +
.../task-card/task-card.component.ts | 15 +
.../task-creation-assignees.component.html | 71 +
.../task-creation-assignees.component.scss | 17 +
.../task-creation-assignees.component.spec.ts | 21 +
.../task-creation-assignees.component.ts | 160 +
.../task-end-date.component.html | 16 +
.../task-end-date.component.scss | 36 +
.../task-end-date.component.spec.ts | 21 +
.../task-end-date/task-end-date.component.ts | 65 +
.../task-labels/task-labels.component.html | 15 +
.../task-labels/task-labels.component.scss | 4 +
.../task-labels/task-labels.component.spec.ts | 21 +
.../task-labels/task-labels.component.ts | 94 +
.../task-members/task-members.component.html | 64 +
.../task-members/task-members.component.scss | 33 +
.../task-members.component.spec.ts | 21 +
.../task-members/task-members.component.ts | 176 +
.../task-name/task-name.component.html | 19 +
.../task-name/task-name.component.scss | 8 +
.../task-name/task-name.component.spec.ts | 21 +
.../task-name/task-name.component.ts | 79 +
.../task-priority.component.html | 13 +
.../task-priority.component.scss | 13 +
.../task-priority.component.spec.ts | 21 +
.../task-priority/task-priority.component.ts | 60 +
.../task-progress.component.html | 9 +
.../task-progress.component.scss | 0
.../task-progress.component.spec.ts | 21 +
.../task-progress/task-progress.component.ts | 65 +
.../task-subtask-count.component.html | 17 +
.../task-subtask-count.component.scss | 4 +
.../task-subtask-count.component.spec.ts | 21 +
.../task-subtask-count.component.ts | 79 +
.../kanban-board/kanban-board.component.html | 306 +
.../kanban-board/kanban-board.component.scss | 401 +
.../kanban-board.component.spec.ts | 21 +
.../kanban-board/kanban-board.component.ts | 1068 +
.../kanban-board/models/board.model.ts | 6 +
.../kanban-board/models/column.model.ts | 14 +
.../pipes/validate-min-date.pipe.ts | 17 +
.../kanban-hash-map-service.service.ts | 69 +
.../kanban-view-v2-routing.module.ts | 15 +
.../kanban-view-v2/kanban-view-v2.module.ts | 121 +
.../kanban-view-v2/kanban-view-v2.service.ts | 100 +
.../gantt-chart-v2-routing.module.ts | 11 +
.../roadmap-v2/gantt-chart-v2.module.ts | 85 +
.../add-task-input.component.html | 21 +
.../add-task-input.component.scss | 25 +
.../add-task-input.component.spec.ts | 21 +
.../add-task-input.component.ts | 160 +
.../add-task-row/add-task-row.component.html | 3 +
.../add-task-row/add-task-row.component.scss | 14 +
.../add-task-row.component.spec.ts | 21 +
.../add-task-row/add-task-row.component.ts | 149 +
.../end-date/end-date.component.html | 16 +
.../end-date/end-date.component.scss | 41 +
.../end-date/end-date.component.spec.ts | 21 +
.../components/end-date/end-date.component.ts | 84 +
.../components/filters/filters.component.html | 20 +
.../components/filters/filters.component.scss | 0
.../filters/filters.component.spec.ts | 21 +
.../components/filters/filters.component.ts | 56 +
.../start-date/start-date.component.html | 16 +
.../start-date/start-date.component.scss | 33 +
.../start-date/start-date.component.spec.ts | 21 +
.../start-date/start-date.component.ts | 72 +
.../task-bar/task-bar.component.html | 21 +
.../task-bar/task-bar.component.scss | 59 +
.../task-bar/task-bar.component.spec.ts | 21 +
.../components/task-bar/task-bar.component.ts | 289 +
.../task-name/task-name.component.html | 25 +
.../task-name/task-name.component.scss | 41 +
.../task-name/task-name.component.spec.ts | 21 +
.../task-name/task-name.component.ts | 96 +
.../directives/drag-move.directive.ts | 138 +
.../project-roadmap-v2-custom.component.html | 207 +
.../project-roadmap-v2-custom.component.scss | 253 +
...roject-roadmap-v2-custom.component.spec.ts | 21 +
.../project-roadmap-v2-custom.component.ts | 355 +
.../services/roadmap-v2-hashmap.service.ts | 208 +
.../services/roadmap-v2-service.service.ts | 414 +
.../modules/task-list-v2/interfaces.ts | 100 +
.../task-list-v2/pipes/end-name-check.pipe.ts | 11 +
.../task-list-columns-toggle.component.html | 25 +
.../task-list-columns-toggle.component.scss | 10 +
...task-list-columns-toggle.component.spec.ts | 23 +
.../task-list-columns-toggle.component.ts | 62 +
.../task-list-filters.component.html | 169 +
.../task-list-filters.component.scss | 27 +
.../task-list-filters.component.spec.ts | 23 +
.../task-list-filters.component.ts | 288 +
.../task-list-hash-map.service.ts | 208 +
.../task-list-add-task-input.component.html | 21 +
.../task-list-add-task-input.component.scss | 14 +
...task-list-add-task-input.component.spec.ts | 23 +
.../task-list-add-task-input.component.ts | 197 +
.../task-list-bulk-actions.component.html | 214 +
.../task-list-bulk-actions.component.scss | 63 +
.../task-list-bulk-actions.component.spec.ts | 23 +
.../task-list-bulk-actions.component.ts | 294 +
.../interfaces/convert-subtask-request.ts | 6 +
.../subtask-convert-service.service.ts | 19 +
.../task-list-context-menu.component.html | 56 +
.../task-list-context-menu.component.scss | 0
.../task-list-context-menu.component.spec.ts | 23 +
.../task-list-context-menu.component.ts | 225 +
.../task-list-group-settings.component.html | 76 +
.../task-list-group-settings.component.scss | 57 +
...task-list-group-settings.component.spec.ts | 23 +
.../task-list-group-settings.component.ts | 236 +
.../task-list-header.component.html | 100 +
.../task-list-header.component.scss | 100 +
.../task-list-header.component.spec.ts | 23 +
.../task-list-header.component.ts | 143 +
.../task-list-phase-duration.component.html | 25 +
.../task-list-phase-duration.component.scss | 4 +
...task-list-phase-duration.component.spec.ts | 21 +
.../task-list-phase-duration.component.ts | 65 +
...-list-phase-settings-drawer.component.html | 97 +
...-list-phase-settings-drawer.component.scss | 63 +
...st-phase-settings-drawer.component.spec.ts | 21 +
...sk-list-phase-settings-drawer.component.ts | 256 +
.../pipes/ellipsis-tooltip-title.pipe.ts | 13 +
.../pipes/sub-tasks-arrow-color.pipe.ts | 11 +
.../pipes/sub-tasks-arrow-icon.pipe.ts | 10 +
.../pipes/truncate-if-long.pipe.ts | 11 +
.../pipes/validate-max-date.pipe.ts | 16 +
.../pipes/validate-min-date.pipe.ts | 16 +
.../task-list-description.component.html | 1 +
.../task-list-description.component.scss | 0
.../task-list-description.component.spec.ts | 23 +
.../task-list-description.component.ts | 44 +
.../task-list-end-date.component.html | 17 +
.../task-list-end-date.component.scss | 30 +
.../task-list-end-date.component.spec.ts | 23 +
.../task-list-end-date.component.ts | 92 +
.../task-list-labels.component.html | 49 +
.../task-list-labels.component.scss | 36 +
.../task-list-labels.component.spec.ts | 23 +
.../task-list-labels.component.ts | 172 +
.../task-list-members.component.html | 61 +
.../task-list-members.component.scss | 17 +
.../task-list-members.component.spec.ts | 23 +
.../task-list-members.component.ts | 211 +
.../task-list-phase.component.html | 20 +
.../task-list-phase.component.scss | 0
.../task-list-phase.component.spec.ts | 21 +
.../task-list-phase.component.ts | 132 +
.../task-list-priority.component.html | 15 +
.../task-list-priority.component.scss | 5 +
.../task-list-priority.component.spec.ts | 23 +
.../task-list-priority.component.ts | 132 +
.../task-list-row.component.html | 222 +
.../task-list-row.component.scss | 381 +
.../task-list-row.component.spec.ts | 23 +
.../task-list-row/task-list-row.component.ts | 299 +
.../task-list-start-date.component.html | 17 +
.../task-list-start-date.component.scss | 25 +
.../task-list-start-date.component.spec.ts | 23 +
.../task-list-start-date.component.ts | 77 +
.../task-list-status.component.html | 14 +
.../task-list-status.component.scss | 5 +
.../task-list-status.component.spec.ts | 23 +
.../task-list-status.component.ts | 143 +
.../task-list-timer.component.html | 53 +
.../task-list-timer.component.scss | 0
.../task-list-timer.component.spec.ts | 23 +
.../task-list-timer.component.ts | 67 +
.../task-progress.component.html | 10 +
.../task-progress.component.scss | 0
.../task-progress.component.spec.ts | 23 +
.../task-progress/task-progress.component.ts | 68 +
.../task-list-table.component.html | 192 +
.../task-list-table.component.scss | 395 +
.../task-list-table.component.spec.ts | 23 +
.../task-list-table.component.ts | 722 +
.../task-list-v2-routing.module.ts | 14 +
.../task-list-v2/task-list-v2.module.ts | 195 +
.../task-list-v2/task-list-v2.service.ts | 418 +
.../activity-log/activity-log.component.html | 20 +
.../activity-log/activity-log.component.scss | 0
.../activity-log.component.spec.ts | 23 +
.../activity-log/activity-log.component.ts | 58 +
.../dashboard/dashboard.component.html | 58 +
.../dashboard/dashboard.component.scss | 37 +
.../dashboard/dashboard.component.spec.ts | 23 +
.../dashboard/dashboard.component.ts | 106 +
.../my-projects/my-projects.component.html | 56 +
.../my-projects/my-projects.component.scss | 87 +
.../my-projects/my-projects.component.spec.ts | 23 +
.../my-projects/my-projects.component.ts | 83 +
.../task-done/task-done.component.html | 5 +
.../task-done/task-done.component.scss | 16 +
.../task-done/task-done.component.spec.ts | 21 +
.../task-done/task-done.component.ts | 39 +
.../task-due-date.component.html | 17 +
.../task-due-date.component.scss | 30 +
.../task-due-date.component.spec.ts | 21 +
.../task-due-date/task-due-date.component.ts | 148 +
.../task-name/task-name.component.html | 27 +
.../task-name/task-name.component.scss | 14 +
.../task-name/task-name.component.spec.ts | 21 +
.../task-name/task-name.component.ts | 54 +
.../task-project/task-project.component.html | 8 +
.../task-project/task-project.component.scss | 4 +
.../task-project.component.spec.ts | 21 +
.../task-project/task-project.component.ts | 12 +
.../task-status/task-status.component.html | 13 +
.../task-status/task-status.component.scss | 3 +
.../task-status/task-status.component.spec.ts | 21 +
.../task-status/task-status.component.ts | 71 +
.../task-add-container.component.html | 95 +
.../task-add-container.component.scss | 24 +
.../task-add-container.component.spec.ts | 21 +
.../task-add-container.component.ts | 252 +
.../tasks-table/tasks-table.component.html | 54 +
.../tasks-table/tasks-table.component.scss | 46 +
.../tasks-table/tasks-table.component.spec.ts | 21 +
.../tasks-table/tasks-table.component.ts | 223 +
.../my-tasks/my-tasks.component.html | 277 +
.../my-tasks/my-tasks.component.scss | 239 +
.../my-tasks/my-tasks.component.spec.ts | 23 +
.../dashboard/my-tasks/my-tasks.component.ts | 118 +
.../personal-tasks.component.html | 37 +
.../personal-tasks.component.scss | 83 +
.../personal-tasks.component.spec.ts | 21 +
.../personal-tasks.component.ts | 79 +
.../my-dashboard/homepage-service.service.ts | 71 +
.../my-dashboard/homepage.enum.ts | 12 +
.../administrator/my-dashboard/intefaces.ts | 25 +
.../my-dashboard-routing.module.ts | 14 +
.../my-dashboard/my-dashboard.module.ts | 124 +
.../personal-todo-list.component.html | 132 +
.../personal-todo-list.component.scss | 18 +
.../personal-todo-list.component.spec.ts | 23 +
.../personal-todo-list.component.ts | 201 +
.../projects-tasks.component.html | 85 +
.../projects-tasks.component.scss | 0
.../projects-tasks.component.spec.ts | 23 +
.../projects-tasks.component.ts | 118 +
.../all-tasks-attachments.component.html | 99 +
.../all-tasks-attachments.component.scss | 9 +
.../all-tasks-attachments.component.spec.ts | 23 +
.../all-tasks-attachments.component.ts | 139 +
.../context-menu/context-menu.component.html | 28 +
.../context-menu/context-menu.component.scss | 0
.../context-menu.component.spec.ts | 21 +
.../context-menu/context-menu.component.ts | 149 +
.../end-date/end-date.component.html | 17 +
.../end-date/end-date.component.scss | 36 +
.../end-date/end-date.component.spec.ts | 21 +
.../components/end-date/end-date.component.ts | 90 +
.../member-task-add-container.component.html | 21 +
.../member-task-add-container.component.scss | 14 +
...ember-task-add-container.component.spec.ts | 21 +
.../member-task-add-container.component.ts | 189 +
.../member-tasks-drawer.component.html | 104 +
.../member-tasks-drawer.component.scss | 40 +
.../member-tasks-drawer.component.spec.ts | 21 +
.../member-tasks-drawer.component.ts | 309 +
.../overview-tab/overview-tab.component.html | 169 +
.../overview-tab/overview-tab.component.scss | 15 +
.../overview-tab.component.spec.ts | 21 +
.../overview-tab/overview-tab.component.ts | 191 +
.../components/phase/phase.component.html | 20 +
.../components/phase/phase.component.scss | 0
.../components/phase/phase.component.spec.ts | 21 +
.../components/phase/phase.component.ts | 114 +
.../priority/priority.component.html | 15 +
.../priority/priority.component.scss | 5 +
.../priority/priority.component.spec.ts | 21 +
.../components/priority/priority.component.ts | 113 +
.../start-date/start-date.component.html | 17 +
.../start-date/start-date.component.scss | 28 +
.../start-date/start-date.component.spec.ts | 21 +
.../start-date/start-date.component.ts | 78 +
.../components/status/status.component.html | 14 +
.../components/status/status.component.scss | 5 +
.../status/status.component.spec.ts | 21 +
.../components/status/status.component.ts | 122 +
.../task-list-header.component.html | 36 +
.../task-list-header.component.scss | 60 +
.../task-list-header.component.spec.ts | 21 +
.../task-list-header.component.ts | 79 +
.../task-list-row.component.html | 104 +
.../task-list-row.component.scss | 232 +
.../task-list-row.component.spec.ts | 21 +
.../task-list-row/task-list-row.component.ts | 188 +
.../task-name/task-name.component.html | 15 +
.../task-name/task-name.component.scss | 47 +
.../task-name/task-name.component.spec.ts | 21 +
.../task-name/task-name.component.ts | 88 +
.../services/wl-tasks-hash-map.service.ts | 173 +
.../services/wl-tasks.service.ts | 264 +
.../workload-gaant-chart-v2.component.html | 248 +
.../workload-gaant-chart-v2.component.scss | 316 +
.../workload-gaant-chart-v2.component.spec.ts | 21 +
.../workload-gaant-chart-v2.component.ts | 338 +
.../project-stats.component.html | 95 +
.../project-stats.component.scss | 20 +
.../project-stats.component.spec.ts | 23 +
.../project-stats/project-stats.component.ts | 71 +
.../project-insights.component.html | 17 +
.../project-insights.component.scss | 25 +
.../project-insights.component.spec.ts | 23 +
.../project-insights.component.ts | 73 +
.../last-updated-tasks.component.html | 39 +
.../last-updated-tasks.component.scss | 5 +
.../last-updated-tasks.component.spec.ts | 23 +
.../last-updated-tasks.component.ts | 96 +
.../priority-breakdown.component.html | 14 +
.../priority-breakdown.component.scss | 0
.../priority-breakdown.component.spec.ts | 23 +
.../priority-breakdown.component.ts | 133 +
.../project-deadline.component.html | 101 +
.../project-deadline.component.scss | 20 +
.../project-deadline.component.spec.ts | 23 +
.../project-deadline.component.ts | 96 +
.../status-overview.component.html | 35 +
.../status-overview.component.scss | 7 +
.../status-overview.component.spec.ts | 23 +
.../status-overview.component.ts | 120 +
.../member-stats/member-stats.component.html | 54 +
.../member-stats/member-stats.component.scss | 20 +
.../member-stats.component.spec.ts | 23 +
.../member-stats/member-stats.component.ts | 64 +
.../member-tasks/member-tasks.component.html | 104 +
.../member-tasks/member-tasks.component.scss | 136 +
.../member-tasks.component.spec.ts | 23 +
.../member-tasks/member-tasks.component.ts | 155 +
...ct-insights-member-overview.component.html | 4 +
...ct-insights-member-overview.component.scss | 4 +
...insights-member-overview.component.spec.ts | 23 +
...ject-insights-member-overview.component.ts | 59 +
.../project-overview.component.html | 31 +
.../project-overview.component.scss | 4 +
.../project-overview.component.spec.ts | 23 +
.../project-overview.component.ts | 153 +
.../task-insights.component.html | 228 +
.../task-insights.component.scss | 9 +
.../task-insights.component.spec.ts | 23 +
.../task-insights/task-insights.component.ts | 232 +
.../project-members.component.html | 111 +
.../project-members.component.scss | 4 +
.../project-members.component.spec.ts | 23 +
.../project-members.component.ts | 147 +
.../project-updates.component.html | 9 +
.../project-updates.component.scss | 5 +
.../project-updates.component.spec.ts | 21 +
.../project-updates.component.ts | 27 +
.../project-sharing-functions.ts | 44 +
.../project-sharing/template.html | 49 +
.../project-view-extra.component.html | 45 +
.../project-view-extra.component.scss | 6 +
.../project-view-extra.component.spec.ts | 23 +
.../project-view-extra.component.ts | 171 +
.../project-view/project-view.component.html | 208 +
.../project-view/project-view.component.scss | 38 +
.../project-view.component.spec.ts | 23 +
.../project-view/project-view.component.ts | 500 +
.../projects/projects-routing.module.ts | 28 +
.../administrator/projects/projects.module.ts | 259 +
.../projects/projects.service.spec.ts | 16 +
.../projects/projects.service.ts | 21 +
.../migrate-member-allocations.component.html | 1 +
.../migrate-member-allocations.component.scss | 0
...grate-member-allocations.component.spec.ts | 21 +
.../migrate-member-allocations.component.ts | 21 +
.../migrate-phase-sort-order.component.html | 1 +
.../migrate-phase-sort-order.component.scss | 0
...migrate-phase-sort-order.component.spec.ts | 21 +
.../migrate-phase-sort-order.component.ts | 19 +
.../migrate-project-phases.component.html | 1 +
.../migrate-project-phases.component.scss | 0
.../migrate-project-phases.component.spec.ts | 21 +
.../migrate-project-phases.component.ts | 21 +
.../migrate-templates.component.html | 1 +
.../migrate-templates.component.scss | 0
.../migrate-templates.component.spec.ts | 21 +
.../migrate-templates.component.ts | 19 +
.../project-filter-by-tooltip.pipe.spec.ts | 8 +
.../pipes/project-filter-by-tooltip.pipe.ts | 10 +
...projects-folder-form-drawer.component.html | 45 +
...projects-folder-form-drawer.component.scss | 0
...jects-folder-form-drawer.component.spec.ts | 21 +
.../projects-folder-form-drawer.component.ts | 133 +
.../projects-folder-form-drawer.service.ts | 20 +
.../projects-folder-view.component.html | 1 +
.../projects-folder-view.component.scss | 0
.../projects-folder-view.component.spec.ts | 21 +
.../projects-folder-view.component.ts | 10 +
.../projects-list-view.component.html | 1 +
.../projects-list-view.component.scss | 0
.../projects-list-view.component.spec.ts | 21 +
.../projects-list-view.component.ts | 10 +
.../projects/projects/projects.component.html | 276 +
.../projects/projects/projects.component.scss | 21 +
.../projects/projects.component.spec.ts | 25 +
.../projects/projects/projects.component.ts | 353 +
.../estimated-vs-actual-chart.component.html | 13 +
.../estimated-vs-actual-chart.component.scss | 19 +
...stimated-vs-actual-chart.component.spec.ts | 21 +
.../estimated-vs-actual-chart.component.ts | 85 +
.../member-logs-breakdown.component.html | 1 +
.../member-logs-breakdown.component.scss | 0
.../member-logs-breakdown.component.spec.ts | 21 +
.../member-logs-breakdown.component.ts | 10 +
.../project-category.component.html | 42 +
.../project-category.component.scss | 43 +
.../project-category.component.spec.ts | 21 +
.../project-category.component.ts | 187 +
.../project-health.component.html | 11 +
.../project-health.component.scss | 0
.../project-health.component.spec.ts | 21 +
.../project-health.component.ts | 56 +
.../project-logs-breakdown.component.html | 100 +
.../project-logs-breakdown.component.scss | 3 +
.../project-logs-breakdown.component.spec.ts | 21 +
.../project-logs-breakdown.component.ts | 208 +
.../project-start-end-dates.component.html | 20 +
.../project-start-end-dates.component.scss | 0
.../project-start-end-dates.component.spec.ts | 21 +
.../project-start-end-dates.component.ts | 73 +
.../project-status.component.html | 10 +
.../project-status.component.scss | 10 +
.../project-status.component.spec.ts | 21 +
.../project-status.component.ts | 61 +
.../rpt-header/rpt-header.component.html | 65 +
.../rpt-header/rpt-header.component.scss | 0
.../rpt-header/rpt-header.component.spec.ts | 21 +
.../rpt-header/rpt-header.component.ts | 122 +
.../rpt-layout/rpt-layout.component.html | 51 +
.../rpt-layout/rpt-layout.component.scss | 114 +
.../rpt-layout/rpt-layout.component.spec.ts | 21 +
.../rpt-layout/rpt-layout.component.ts | 73 +
.../rpt-drawer-title.component.html | 5 +
.../rpt-drawer-title.component.scss | 8 +
.../rpt-drawer-title.component.spec.ts | 21 +
.../rpt-drawer-title.component.ts | 16 +
.../rpt-flat-task-list.component.html | 61 +
.../rpt-flat-task-list.component.scss | 4 +
.../rpt-flat-task-list.component.spec.ts | 21 +
.../rpt-flat-task-list.component.ts | 194 +
.../rpt-flat-tasks-list.module.ts | 44 +
.../rpt-grouped-task-list.component.html | 73 +
.../rpt-grouped-task-list.component.scss | 4 +
.../rpt-grouped-task-list.component.spec.ts | 21 +
.../rpt-grouped-task-list.component.ts | 132 +
.../rpt-grouped-task-list.module.ts | 46 +
.../rpt-member-projects-list.component.html | 50 +
.../rpt-member-projects-list.component.scss | 0
...rpt-member-projects-list.component.spec.ts | 21 +
.../rpt-member-projects-list.component.ts | 81 +
.../rpt-projects-list.component.html | 168 +
.../rpt-projects-list.component.scss | 27 +
.../rpt-projects-list.component.spec.ts | 21 +
.../rpt-projects-list.component.ts | 242 +
.../rpt-projects-list.module.ts | 63 +
.../drawers/reporting-drawers.service.ts | 132 +
.../rpt-member-drawer-overview.component.html | 164 +
.../rpt-member-drawer-overview.component.scss | 14 +
...t-member-drawer-overview.component.spec.ts | 21 +
.../rpt-member-drawer-overview.component.ts | 194 +
.../rpt-member-drawer-projects.component.html | 6 +
.../rpt-member-drawer-projects.component.scss | 0
...t-member-drawer-projects.component.spec.ts | 21 +
.../rpt-member-drawer-projects.component.ts | 20 +
.../rpt-member-drawer-tasks.component.html | 5 +
.../rpt-member-drawer-tasks.component.scss | 0
.../rpt-member-drawer-tasks.component.spec.ts | 21 +
.../rpt-member-drawer-tasks.component.ts | 12 +
.../rpt-member-drawer.component.html | 69 +
.../rpt-member-drawer.component.scss | 0
.../rpt-member-drawer.component.spec.ts | 21 +
.../rpt-member-drawer.component.ts | 75 +
.../rpt-member-drawer.module.ts | 70 +
.../rpt-project-drawer-members.component.html | 45 +
.../rpt-project-drawer-members.component.scss | 0
...t-project-drawer-members.component.spec.ts | 21 +
.../rpt-project-drawer-members.component.ts | 65 +
...rpt-project-drawer-overview.component.html | 204 +
...rpt-project-drawer-overview.component.scss | 25 +
...-project-drawer-overview.component.spec.ts | 21 +
.../rpt-project-drawer-overview.component.ts | 193 +
.../rpt-project-drawer-tasks.component.html | 3 +
.../rpt-project-drawer-tasks.component.scss | 0
...rpt-project-drawer-tasks.component.spec.ts | 21 +
.../rpt-project-drawer-tasks.component.ts | 11 +
.../rpt-project-drawer.component.html | 63 +
.../rpt-project-drawer.component.scss | 0
.../rpt-project-drawer.component.spec.ts | 21 +
.../rpt-project-drawer.component.ts | 75 +
.../rpt-project-drawer.module.ts | 71 +
.../activity-logs.component.html | 131 +
.../activity-logs.component.scss | 25 +
.../activity-logs.component.spec.ts | 21 +
.../activity-logs/activity-logs.component.ts | 128 +
.../duration-filter.component.html | 34 +
.../duration-filter.component.scss | 0
.../duration-filter.component.spec.ts | 21 +
.../duration-filter.component.ts | 66 +
...ngle-member-drawer-overview.component.html | 181 +
...ngle-member-drawer-overview.component.scss | 16 +
...e-member-drawer-overview.component.spec.ts | 21 +
...single-member-drawer-overview.component.ts | 283 +
.../service/log-header.service.ts | 34 +
.../single-member-time-logs.component.html | 38 +
.../single-member-time-logs.component.scss | 18 +
.../single-member-time-logs.component.spec.ts | 21 +
.../single-member-time-logs.component.ts | 139 +
.../rpt-single-member-drawer.component.html | 79 +
.../rpt-single-member-drawer.component.scss | 0
...rpt-single-member-drawer.component.spec.ts | 21 +
.../rpt-single-member-drawer.component.ts | 268 +
.../rpt-single-member-drawer.module.ts | 79 +
...ngle-member-projects-drawer.component.html | 96 +
...ngle-member-projects-drawer.component.scss | 8 +
...e-member-projects-drawer.component.spec.ts | 21 +
...single-member-projects-drawer.component.ts | 84 +
.../rpt-single-member-stat.component.html | 84 +
.../rpt-single-member-stat.component.scss | 11 +
.../rpt-single-member-stat.component.spec.ts | 21 +
.../rpt-single-member-stat.component.ts | 146 +
.../rpt-task-view-drawer.component.html | 6 +
.../rpt-task-view-drawer.component.scss | 0
.../rpt-task-view-drawer.component.spec.ts | 21 +
.../rpt-task-view-drawer.component.ts | 82 +
.../rpt-task-view-drawer.module.ts | 19 +
.../rpt-tasks-drawer.component.html | 49 +
.../rpt-tasks-drawer.component.scss | 0
.../rpt-tasks-drawer.component.spec.ts | 21 +
.../rpt-tasks-drawer.component.ts | 61 +
.../rpt-tasks-drawer.module.ts | 37 +
.../rpt-team-drawer-members.component.html | 36 +
.../rpt-team-drawer-members.component.scss | 0
.../rpt-team-drawer-members.component.spec.ts | 21 +
.../rpt-team-drawer-members.component.ts | 71 +
.../rpt-team-drawer-projects.component.html | 6 +
.../rpt-team-drawer-projects.component.scss | 0
...rpt-team-drawer-projects.component.spec.ts | 21 +
.../rpt-team-drawer-projects.component.ts | 16 +
.../rpt-team-drawer.component.html | 59 +
.../rpt-team-drawer.component.scss | 0
.../rpt-team-drawer.component.spec.ts | 21 +
.../rpt-team-drawer.component.ts | 89 +
.../rpt-team-drawer/rpt-team-drawer.module.ts | 63 +
.../rpt-team-overview.component.html | 174 +
.../rpt-team-overview.component.scss | 9 +
.../rpt-team-overview.component.spec.ts | 21 +
.../rpt-team-overview.component.ts | 181 +
.../app/administrator/reporting/interfaces.ts | 422 +
.../rpt-allocation-routing.module.ts | 14 +
.../rpt-allocation/rpt-allocation.module.ts | 52 +
.../rpt-allocation.component.html | 160 +
.../rpt-allocation.component.scss | 95 +
.../rpt-allocation.component.spec.ts | 21 +
.../rpt-allocation.component.ts | 301 +
.../rpt-members/rpt-members-routing.module.ts | 14 +
.../modules/rpt-members/rpt-members.module.ts | 77 +
.../rpt-members/rpt-members.component.html | 137 +
.../rpt-members/rpt-members.component.scss | 0
.../rpt-members/rpt-members.component.spec.ts | 21 +
.../rpt-members/rpt-members.component.ts | 381 +
.../rpt-overview-routing.module.ts | 14 +
.../rpt-overview/rpt-overview.module.ts | 81 +
.../rpt-overview-cards.component.html | 54 +
.../rpt-overview-cards.component.scss | 3 +
.../rpt-overview-cards.component.spec.ts | 21 +
.../rpt-overview-cards.component.ts | 48 +
.../rpt-overview/rpt-overview.component.html | 38 +
.../rpt-overview/rpt-overview.component.scss | 0
.../rpt-overview.component.spec.ts | 21 +
.../rpt-overview/rpt-overview.component.ts | 66 +
.../rpt-projects-routing.module.ts | 14 +
.../rpt-projects/rpt-projects.module.ts | 97 +
...rojects-estimated-vs-actual.component.html | 11 +
...rojects-estimated-vs-actual.component.scss | 16 +
...ects-estimated-vs-actual.component.spec.ts | 21 +
...-projects-estimated-vs-actual.component.ts | 86 +
.../rpt-projects/rpt-projects.component.html | 297 +
.../rpt-projects/rpt-projects.component.scss | 27 +
.../rpt-projects.component.spec.ts | 21 +
.../rpt-projects/rpt-projects.component.ts | 400 +
...ime-estimation-vs-actual-routing.module.ts | 17 +
.../rpt-time-estimation-vs-actual.module.ts | 53 +
...timation-vs-actual-projects.component.html | 137 +
...timation-vs-actual-projects.component.scss | 11 +
...ation-vs-actual-projects.component.spec.ts | 21 +
...estimation-vs-actual-projects.component.ts | 431 +
.../rpt-time-members-routing.module.ts | 15 +
.../rpt-time-members.module.ts | 43 +
.../time-members/time-members.component.html | 127 +
.../time-members/time-members.component.scss | 6 +
.../time-members.component.spec.ts | 21 +
.../time-members/time-members.component.ts | 417 +
.../rpt-time-projects-routing.module.ts | 15 +
.../rpt-time-projects.module.ts | 52 +
.../time-projects.component.html | 129 +
.../time-projects.component.scss | 6 +
.../time-projects.component.spec.ts | 21 +
.../time-projects/time-projects.component.ts | 420 +
.../reporting/pipes/with-count.pipe.spec.ts | 8 +
.../reporting/pipes/with-count.pipe.ts | 10 +
.../reporting/reporting-api.service.ts | 179 +
.../reporting/reporting-routing.module.ts | 58 +
.../reporting/reporting-service.service.ts | 36 +
.../reporting/reporting.module.ts | 127 +
.../reporting/reporting.service.ts | 122 +
.../project-schedule.component.html | 164 +
.../project-schedule.component.scss | 220 +
.../project-schedule.component.spec.ts | 23 +
.../project-schedule.component.ts | 116 +
.../schedule/schedule-routing.module.ts | 14 +
.../add-member-allocation.component.html | 3 +
.../add-member-allocation.component.scss | 14 +
.../add-member-allocation.component.spec.ts | 21 +
.../add-member-allocation.component.ts | 105 +
.../context-menu/context-menu.component.html | 8 +
.../context-menu/context-menu.component.scss | 0
.../context-menu.component.spec.ts | 21 +
.../context-menu/context-menu.component.ts | 67 +
.../end-date/end-date.component.html | 17 +
.../end-date/end-date.component.scss | 36 +
.../end-date/end-date.component.spec.ts | 21 +
.../components/end-date/end-date.component.ts | 90 +
.../member-indicator.component.html | 13 +
.../member-indicator.component.scss | 54 +
.../member-indicator.component.spec.ts | 21 +
.../member-indicator.component.ts | 159 +
.../member-task-add-container.component.html | 21 +
.../member-task-add-container.component.scss | 14 +
...ember-task-add-container.component.spec.ts | 21 +
.../member-task-add-container.component.ts | 193 +
.../components/phase/phase.component.html | 20 +
.../components/phase/phase.component.scss | 0
.../components/phase/phase.component.spec.ts | 21 +
.../components/phase/phase.component.ts | 114 +
.../priority/priority.component.html | 15 +
.../priority/priority.component.scss | 5 +
.../priority/priority.component.spec.ts | 21 +
.../components/priority/priority.component.ts | 114 +
.../project-indicator.component.html | 8 +
.../project-indicator.component.scss | 10 +
.../project-indicator.component.spec.ts | 21 +
.../project-indicator.component.ts | 33 +
...project-member-tasks-drawer.component.html | 92 +
...project-member-tasks-drawer.component.scss | 40 +
...ject-member-tasks-drawer.component.spec.ts | 21 +
.../project-member-tasks-drawer.component.ts | 282 +
.../start-date/start-date.component.html | 17 +
.../start-date/start-date.component.scss | 28 +
.../start-date/start-date.component.spec.ts | 21 +
.../start-date/start-date.component.ts | 79 +
.../components/status/status.component.html | 14 +
.../components/status/status.component.scss | 5 +
.../status/status.component.spec.ts | 21 +
.../components/status/status.component.ts | 122 +
.../task-add-input.component.html | 21 +
.../task-add-input.component.scss | 25 +
.../task-add-input.component.spec.ts | 21 +
.../task-add-input.component.ts | 156 +
.../task-add-row/task-add-row.component.html | 3 +
.../task-add-row/task-add-row.component.scss | 14 +
.../task-add-row.component.spec.ts | 21 +
.../task-add-row/task-add-row.component.ts | 76 +
.../task-list-header.component.html | 36 +
.../task-list-header.component.scss | 60 +
.../task-list-header.component.spec.ts | 21 +
.../task-list-header.component.ts | 53 +
.../task-list-row.component.html | 83 +
.../task-list-row.component.scss | 232 +
.../task-list-row.component.spec.ts | 21 +
.../task-list-row/task-list-row.component.ts | 193 +
.../task-name/task-name.component.html | 15 +
.../task-name/task-name.component.scss | 47 +
.../task-name/task-name.component.spec.ts | 21 +
.../task-name/task-name.component.ts | 85 +
.../tasks-context-menu.component.html | 28 +
.../tasks-context-menu.component.scss | 0
.../tasks-context-menu.component.spec.ts | 21 +
.../tasks-context-menu.component.ts | 151 +
.../projects-schedule.component.html | 149 +
.../projects-schedule.component.scss | 261 +
.../projects-schedule.component.spec.ts | 21 +
.../projects-schedule.component.ts | 225 +
.../project-schedule-service.service.ts | 136 +
...le-member-tasks-hashmap-service.service.ts | 172 +
.../schedule-member-tasks-service.service.ts | 264 +
.../service/scheduler-common.service.ts | 35 +
.../schedule-view.component.html | 36 +
.../schedule-view.component.scss | 3 +
.../schedule-view.component.spec.ts | 23 +
.../schedule-view/schedule-view.component.ts | 64 +
.../administrator/schedule/schedule.module.ts | 120 +
.../team-schedule.component.html | 155 +
.../team-schedule.component.scss | 220 +
.../team-schedule.component.spec.ts | 23 +
.../team-schedule/team-schedule.component.ts | 113 +
.../categories/categories-routing.module.ts | 16 +
.../settings/categories/categories.module.ts | 41 +
.../categories/categories.component.html | 68 +
.../categories/categories.component.scss | 0
.../categories/categories.component.spec.ts | 21 +
.../categories/categories.component.ts | 95 +
.../change-password.component.html | 68 +
.../change-password.component.scss | 3 +
.../change-password.component.spec.ts | 23 +
.../change-password.component.ts | 68 +
.../clients/clients-routing.module.ts | 14 +
.../settings/clients/clients.module.ts | 58 +
.../clients/clients/clients.component.html | 93 +
.../clients/clients/clients.component.scss | 0
.../clients/clients/clients.component.spec.ts | 25 +
.../clients/clients/clients.component.ts | 181 +
.../job-titles/job-titles-routing.module.ts | 14 +
.../settings/job-titles/job-titles.module.ts | 57 +
.../job-titles/job-titles.component.html | 86 +
.../job-titles/job-titles.component.scss | 0
.../job-titles/job-titles.component.spec.ts | 25 +
.../job-titles/job-titles.component.ts | 183 +
.../settings/labels/labels.component.html | 67 +
.../settings/labels/labels.component.scss | 0
.../settings/labels/labels.component.spec.ts | 23 +
.../settings/labels/labels.component.ts | 96 +
.../language-and-region.component.html | 35 +
.../language-and-region.component.scss | 0
.../language-and-region.component.spec.ts | 23 +
.../language-and-region.component.ts | 70 +
.../notification-settings-routing.module.ts | 14 +
.../notification-settings.module.ts | 28 +
.../notification-settings.component.html | 67 +
.../notification-settings.component.scss | 8 +
.../notification-settings.component.spec.ts | 23 +
.../notification-settings.component.ts | 79 +
.../settings/profile/profile.component.html | 59 +
.../settings/profile/profile.component.scss | 15 +
.../profile/profile.component.spec.ts | 23 +
.../settings/profile/profile.component.ts | 133 +
.../add-task-input.component.html | 21 +
.../add-task-input.component.scss | 0
.../add-task-input.component.spec.ts | 21 +
.../add-task-input.component.ts | 173 +
.../context-menu/context-menu.component.html | 29 +
.../context-menu/context-menu.component.scss | 0
.../context-menu.component.spec.ts | 21 +
.../context-menu/context-menu.component.ts | 126 +
.../group-filter/group-filter.component.html | 64 +
.../group-filter/group-filter.component.scss | 0
.../group-filter.component.spec.ts | 21 +
.../group-filter/group-filter.component.ts | 109 +
.../phase-settings-drawer.component.html | 78 +
.../phase-settings-drawer.component.scss | 0
.../phase-settings-drawer.component.spec.ts | 21 +
.../phase-settings-drawer.component.ts | 198 +
.../task-description.component.html | 35 +
.../task-description.component.scss | 36 +
.../task-description.component.spec.ts | 21 +
.../task-description.component.ts | 87 +
.../task-end-date.component.html | 17 +
.../task-end-date.component.scss | 0
.../task-end-date.component.spec.ts | 21 +
.../task-end-date/task-end-date.component.ts | 74 +
.../task-estimation.component.html | 41 +
.../task-estimation.component.scss | 0
.../task-estimation.component.spec.ts | 21 +
.../task-estimation.component.ts | 76 +
.../task-labels/task-labels.component.html | 49 +
.../task-labels/task-labels.component.scss | 36 +
.../task-labels/task-labels.component.spec.ts | 21 +
.../row/task-labels/task-labels.component.ts | 167 +
.../row/task-phase/task-phase.component.html | 20 +
.../row/task-phase/task-phase.component.scss | 0
.../task-phase/task-phase.component.spec.ts | 21 +
.../row/task-phase/task-phase.component.ts | 130 +
.../task-priority.component.html | 15 +
.../task-priority.component.scss | 0
.../task-priority.component.spec.ts | 21 +
.../task-priority/task-priority.component.ts | 127 +
.../task-start-date.component.html | 18 +
.../task-start-date.component.scss | 0
.../task-start-date.component.spec.ts | 21 +
.../task-start-date.component.ts | 63 +
.../task-status/task-status.component.html | 14 +
.../task-status/task-status.component.scss | 3 +
.../task-status/task-status.component.spec.ts | 21 +
.../row/task-status/task-status.component.ts | 127 +
.../status-settings-drawer.component.html | 41 +
.../status-settings-drawer.component.scss | 0
.../status-settings-drawer.component.spec.ts | 21 +
.../status-settings-drawer.component.ts | 149 +
.../task-list-group-settings.component.html | 58 +
.../task-list-group-settings.component.scss | 42 +
...task-list-group-settings.component.spec.ts | 21 +
.../task-list-group-settings.component.ts | 215 +
.../task-list-header.component.html | 60 +
.../task-list-header.component.scss | 84 +
.../task-list-header.component.spec.ts | 21 +
.../task-list-header.component.ts | 83 +
.../template-name.component.html | 10 +
.../template-name.component.scss | 27 +
.../template-name.component.spec.ts | 21 +
.../template-name/template-name.component.ts | 87 +
.../project-template-edit-view/interfaces.ts | 111 +
.../pipes/ellipsis-tooltip-title.pipe.ts | 13 +
.../pipes/end-name-check.pipe.ts | 11 +
.../pipes/sub-tasks-arrow-color.pipe.ts | 11 +
.../pipes/sub-tasks-arrow-icon.pipe.ts | 10 +
.../pipes/truncate-if-long.pipe.ts | 11 +
.../project-template-edit-view.component.html | 147 +
.../project-template-edit-view.component.scss | 403 +
...oject-template-edit-view.component.spec.ts | 21 +
.../project-template-edit-view.component.ts | 488 +
.../services/pt-task-list-hash-map.service.ts | 205 +
.../services/pt-task-list.service.ts | 308 +
.../task-list-row.component.html | 120 +
.../task-list-row.component.scss | 360 +
.../task-list-row.component.spec.ts | 21 +
.../task-list-row/task-list-row.component.ts | 194 +
.../project-templates.component.html | 37 +
.../project-templates.component.scss | 0
.../project-templates.component.spec.ts | 21 +
.../project-templates.component.ts | 63 +
.../settings/settings-routing.module.ts | 64 +
.../administrator/settings/settings.module.ts | 182 +
.../settings/settings.service.spec.ts | 16 +
.../settings/settings.service.ts | 19 +
.../settings/settings/settings.component.html | 23 +
.../settings/settings/settings.component.scss | 3 +
.../settings/settings.component.spec.ts | 25 +
.../settings/settings/settings.component.ts | 57 +
.../task-templates.component.html | 43 +
.../task-templates.component.scss | 0
.../task-templates.component.spec.ts | 23 +
.../task-templates.component.ts | 86 +
.../team-members/team-members.component.html | 123 +
.../team-members/team-members.component.scss | 0
.../team-members.component.spec.ts | 23 +
.../team-members/team-members.component.ts | 168 +
.../settings/teams/teams.component.html | 90 +
.../settings/teams/teams.component.scss | 4 +
.../settings/teams/teams.component.spec.ts | 23 +
.../settings/teams/teams.component.ts | 94 +
.../src/app/app-routing.module.ts | 34 +
worklenz-frontend/src/app/app.component.html | 1 +
worklenz-frontend/src/app/app.component.scss | 0
.../src/app/app.component.spec.ts | 35 +
worklenz-frontend/src/app/app.component.ts | 44 +
worklenz-frontend/src/app/app.module.ts | 68 +
.../src/app/auth/auth-routing.module.ts | 34 +
worklenz-frontend/src/app/auth/auth.module.ts | 56 +
.../src/app/auth/layout/layout.component.html | 19 +
.../src/app/auth/layout/layout.component.scss | 25 +
.../app/auth/layout/layout.component.spec.ts | 25 +
.../src/app/auth/layout/layout.component.ts | 42 +
.../src/app/auth/login/login.component.html | 62 +
.../src/app/auth/login/login.component.scss | 11 +
.../app/auth/login/login.component.spec.ts | 25 +
.../src/app/auth/login/login.component.ts | 150 +
.../reset-password.component.html | 55 +
.../reset-password.component.scss | 0
.../reset-password.component.spec.ts | 25 +
.../reset-password.component.ts | 78 +
.../src/app/auth/signup/signup.component.html | 79 +
.../src/app/auth/signup/signup.component.scss | 0
.../app/auth/signup/signup.component.spec.ts | 25 +
.../src/app/auth/signup/signup.component.ts | 170 +
.../auth/team-name/team-name.component.html | 19 +
.../auth/team-name/team-name.component.scss | 0
.../team-name/team-name.component.spec.ts | 25 +
.../app/auth/team-name/team-name.component.ts | 84 +
.../verify-reset-email.component.html | 51 +
.../verify-reset-email.component.scss | 0
.../verify-reset-email.component.spec.ts | 23 +
.../verify-reset-email.component.ts | 74 +
.../authenticate/authenticate.component.html | 1 +
.../authenticate/authenticate.component.scss | 0
.../authenticate.component.spec.ts | 25 +
.../authenticate/authenticate.component.ts | 57 +
.../infinite-scroll-trigger.directive.spec.ts | 9 +
.../infinite-scroll-trigger.directive.ts | 100 +
.../app/directives/lazy-for.directive.spec.ts | 8 +
.../src/app/directives/lazy-for.directive.ts | 194 +
.../not-authorized.component.html | 5 +
.../not-authorized.component.scss | 0
.../not-authorized.component.spec.ts | 23 +
.../not-authorized.component.ts | 19 +
.../errors/not-found/not-found.component.html | 5 +
.../errors/not-found/not-found.component.scss | 0
.../not-found/not-found.component.spec.ts | 23 +
.../errors/not-found/not-found.component.ts | 19 +
.../src/app/guards/auth.guard.spec.ts | 16 +
.../src/app/guards/auth.guard.ts | 30 +
.../src/app/guards/login-check.guard.spec.ts | 16 +
.../src/app/guards/login-check.guard.ts | 29 +
.../guards/non-google-account.guard.spec.ts | 16 +
.../app/guards/non-google-account.guard.ts | 28 +
.../src/app/guards/team-member.guard.spec.ts | 16 +
.../src/app/guards/team-member.guard.ts | 31 +
.../src/app/guards/team-name.guard.spec.ts | 16 +
.../src/app/guards/team-name.guard.ts | 29 +
.../team-owner-or-admin-guard.service.ts | 30 +
.../guards/team-owner-or-admin.guard.spec.ts | 16 +
.../src/app/guards/team-owner.guard.spec.ts | 16 +
.../src/app/guards/team-owner.guard.ts | 30 +
.../src/app/guards/team-url.guard.spec.ts | 16 +
.../src/app/guards/team-url.guard.ts | 19 +
.../app/interceptors/http.interceptor.spec.ts | 16 +
.../src/app/interceptors/http.interceptor.ts | 80 +
.../src/app/interfaces/account-center.ts | 193 +
.../app/interfaces/allocation-view-model.ts | 17 +
.../app/interfaces/api-models/activity-log.ts | 5 +
.../api-models/activity-logs-get-response.ts | 59 +
.../api-models/authorize-response.ts | 8 +
.../api-models/bulk-assign-request.ts | 12 +
.../api-models/bulk-delete-tasks-request.ts | 2 +
.../api-models/bulk-tasks-archive-request.ts | 4 +
.../api-models/bulk-tasks-delete-request.ts | 3 +
.../api-models/bulk-tasks-delete-response.ts | 7 +
.../api-models/bulk-tasks-labels-request.ts | 7 +
.../bulk-tasks-phase-change-request.ts | 4 +
.../bulk-tasks-priority-change-request.ts | 4 +
.../bulk-tasks-status-change-request.ts | 4 +
.../api-models/client-view-model.ts | 5 +
.../api-models/clients-view-model.ts | 6 +
.../src/app/interfaces/api-models/gantt.ts | 57 +
.../interfaces/api-models/inline-member.ts | 8 +
.../api-models/job-titles-view-model.ts | 6 +
.../interfaces/api-models/local-session.ts | 28 +
.../my-dashboard-all-tasks-view-model.ts | 33 +
.../organization-team-get-request.ts | 7 +
.../organization-users-get-request.ts | 6 +
.../project-comment-create-request.ts | 10 +
.../api-models/project-create-request.ts | 6 +
.../interfaces/api-models/project-insights.ts | 54 +
.../api-models/project-members-view-model.ts | 18 +
.../api-models/project-tasks-view-model.ts | 89 +
.../interfaces/api-models/project-template.ts | 45 +
.../api-models/project-view-model.ts | 43 +
.../api-models/projects-get-response.ts | 19 +
.../api-models/projects-view-model.ts | 6 +
.../app/interfaces/api-models/reporting.ts | 24 +
.../api-models/reset-password-request.ts | 3 +
.../interfaces/api-models/server-response.ts | 6 +
.../api-models/task-attachment-view-model.ts | 16 +
.../interfaces/api-models/task-attachment.ts | 8 +
.../api-models/task-comment-view-model.ts | 8 +
.../task-comments-create-request.ts.ts | 5 +
.../api-models/task-create-request.ts | 25 +
.../api-models/task-get-response.ts | 23 +
.../interfaces/api-models/task-list-config.ts | 18 +
.../api-models/task-log-create-request.ts | 19 +
.../app/interfaces/api-models/task-phase.ts | 6 +
.../task-priorities-get-response.ts | 5 +
.../api-models/task-status-create-request.ts | 9 +
.../api-models/task-status-get-response.ts | 6 +
.../api-models/task-status-update-model.ts | 5 +
.../api-models/task-template-get-response.ts | 7 +
.../api-models/task-templates-get-response.ts | 5 +
.../api-models/team-activate-response.ts | 3 +
.../api-models/team-get-response.ts | 7 +
.../api-models/team-invitation-view-model.ts | 6 +
.../api-models/team-member-create-request.ts | 7 +
.../api-models/team-members-get-response.ts | 78 +
.../api-models/team-members-view-model.ts | 6 +
.../interfaces/api-models/team-view-model.ts | 9 +
.../api-models/user-login-request.ts | 6 +
.../api-models/user-login-response.ts | 4 +
.../api-models/user-sign-up-request.ts | 10 +
.../api-models/user-sign-up-response.ts | 4 +
.../api-models/verify-reset-email.ts | 5 +
.../src/app/interfaces/avatar-attachment.ts | 5 +
.../src/app/interfaces/client.ts | 7 +
.../src/app/interfaces/gantt-chart.ts | 69 +
.../src/app/interfaces/invitation-response.ts | 5 +
.../src/app/interfaces/job-title.ts | 5 +
.../src/app/interfaces/my-tasks.ts | 13 +
.../src/app/interfaces/nav-item-type.ts | 4 +
.../src/app/interfaces/nav-item.ts | 8 +
.../app/interfaces/notification-settings.ts | 6 +
.../src/app/interfaces/notification.ts | 14 +
.../app/interfaces/pagination-component.ts | 13 +
.../app/interfaces/password-strength-check.ts | 76 +
.../interfaces/password-validity-result.ts | 6 +
.../src/app/interfaces/personal-overview.ts | 13 +
.../src/app/interfaces/profile-settings.ts | 5 +
.../app/interfaces/project-access-level.ts | 5 +
.../src/app/interfaces/project-category.ts | 14 +
.../src/app/interfaces/project-comments.ts | 11 +
.../src/app/interfaces/project-folder.ts | 11 +
.../src/app/interfaces/project-health.ts | 8 +
.../src/app/interfaces/project-manager.ts | 6 +
.../src/app/interfaces/project-member.ts | 6 +
.../src/app/interfaces/project-status.ts | 9 +
.../src/app/interfaces/project-template.ts | 17 +
.../project-wise-resources-view-model.ts | 56 +
.../src/app/interfaces/project.ts | 44 +
.../app/interfaces/projects-filter-config.ts | 10 +
.../reporting-allocation-settings.ts | 5 +
.../src/app/interfaces/reporting.ts | 219 +
.../src/app/interfaces/roadmap.ts | 36 +
worklenz-frontend/src/app/interfaces/role.ts | 7 +
.../src/app/interfaces/schedular.ts | 92 +
.../src/app/interfaces/selectable-category.ts | 5 +
.../src/app/interfaces/selectable-project.ts | 5 +
.../src/app/interfaces/selectable-team.ts | 5 +
.../interfaces/settings-navigation-item.ts | 5 +
.../src/app/interfaces/sub-task.ts | 18 +
.../task-assignee-update-response.ts | 13 +
.../src/app/interfaces/task-assignee.ts | 6 +
.../src/app/interfaces/task-comment.ts | 9 +
.../app/interfaces/task-form-view-model.ts | 57 +
.../src/app/interfaces/task-label.ts | 10 +
.../src/app/interfaces/task-list-column.ts | 8 +
.../task-list-estimation-change-response.ts | 8 +
.../task-list-status-change-response.ts | 14 +
.../interfaces/task-phase-change-response.ts | 6 +
.../src/app/interfaces/task-priority.ts | 6 +
.../app/interfaces/task-status-category.ts | 6 +
.../src/app/interfaces/task-status.ts | 23 +
worklenz-frontend/src/app/interfaces/task.ts | 48 +
.../src/app/interfaces/team-member.ts | 12 +
worklenz-frontend/src/app/interfaces/team.ts | 21 +
.../interfaces/timer-start-event-response.ts | 5 +
.../interfaces/timer-stop-event-response.ts | 4 +
.../src/app/interfaces/timezone.ts | 6 +
.../src/app/interfaces/todo-list-item.ts | 10 +
worklenz-frontend/src/app/interfaces/user.ts | 6 +
.../app/interfaces/worklenz-notification.ts | 14 +
.../src/app/interfaces/workload.ts | 91 +
.../src/app/pipes/bind-na.pipe.spec.ts | 8 +
.../src/app/pipes/bind-na.pipe.ts | 11 +
.../src/app/pipes/date-formatter.pipe.ts | 35 +
.../src/app/pipes/ellipsis.pipe.spec.ts | 8 +
.../src/app/pipes/ellipsis.pipe.ts | 15 +
.../app/pipes/first-char-upper.pipe.spec.ts | 8 +
.../src/app/pipes/first-char-upper.pipe.ts | 12 +
.../src/app/pipes/from-now.pipe.spec.ts | 8 +
.../src/app/pipes/from-now.pipe.ts | 13 +
.../src/app/pipes/min-date-validator.pipe.ts | 17 +
.../src/app/pipes/safe-string.pipe.spec.ts | 8 +
.../src/app/pipes/safe-string.pipe.ts | 16 +
.../src/app/pipes/search-by-name.pipe.spec.ts | 8 +
.../src/app/pipes/search-by-name.pipe.ts | 16 +
.../pipes/task-comment-mention.pipe.spec.ts | 8 +
.../app/pipes/task-comment-mention.pipe.ts | 29 +
.../src/app/pipes/to-now.pipe.spec.ts | 8 +
.../src/app/pipes/to-now.pipe.ts | 13 +
.../src/app/pipes/with-alpha.pipe.spec.ts | 8 +
.../src/app/pipes/with-alpha.pipe.ts | 13 +
.../src/app/pipes/wl-safe-array.pipe.spec.ts | 8 +
.../src/app/pipes/wl-safe-array.pipe.ts | 11 +
.../api/access-controls-api.service.ts | 8 +
.../api/account-center-api.service.ts | 90 +
.../api/activity-logs.service.spec.ts | 16 +
.../app/services/api/activity-logs.service.ts | 22 +
.../src/app/services/api/api-service-base.ts | 24 +
.../services/api/attachments-api.service.ts | 47 +
.../src/app/services/api/auth-api.service.ts | 57 +
.../app/services/api/clients-api.service.ts | 43 +
.../services/api/gantt-api.service.spec.ts | 16 +
.../src/app/services/api/gantt-api.service.ts | 32 +
.../app/services/api/home-page-api.service.ts | 55 +
.../services/api/job-titles-api.service.ts | 44 +
.../src/app/services/api/logs-api.service.ts | 22 +
.../services/api/notifications-api.service.ts | 37 +
.../services/api/personal-overview.service.ts | 36 +
.../api/profile-settings-api.service.ts | 49 +
.../api/project-categories-api.service.ts | 44 +
.../api/project-categories.service.spec.ts | 16 +
.../api/project-comments-api.service.ts | 50 +
.../api/project-folders-api.service.spec.ts | 16 +
.../api/project-folders-api.service.ts | 39 +
.../api/project-healths-api.service.ts | 23 +
.../services/api/project-insights.service.ts | 72 +
.../api/project-managers-api.service.ts | 24 +
.../api/project-members-api.service.ts | 38 +
.../api/project-roadmap-api.service.ts | 31 +
.../api/project-statuses-api.service.ts | 22 +
.../api/project-template-api.service.ts | 61 +
.../api/project-workload-api.service.ts | 46 +
.../app/services/api/projects-api.service.ts | 134 +
.../app/services/api/pt-labels-api.service.ts | 25 +
.../services/api/pt-priorities-api.service.ts | 26 +
.../services/api/pt-statuses-api.service.ts | 49 +
.../api/pt-task-phases-api.service.ts | 55 +
.../app/services/api/pt-tasks-api.service.ts | 41 +
.../services/api/reporting-api-v0.service.ts | 187 +
.../api/reporting-export-api.service.ts | 154 +
.../api/resource-allocation.service.ts | 27 +
.../app/services/api/schedule-api.service.ts | 59 +
.../api/shared-projects-api.service.ts | 35 +
.../app/services/api/sub-tasks-api.service.ts | 34 +
.../services/api/task-comments-api.service.ts | 35 +
.../services/api/task-labels-api.service.ts | 41 +
.../services/api/task-phases-api.service.ts | 60 +
.../services/api/task-priorities.service.ts | 28 +
.../services/api/task-statuses-api.service.ts | 62 +
.../services/api/task-templates.service.ts | 56 +
.../src/app/services/api/tasks-api.service.ts | 190 +
.../services/api/tasks-log-time.service.ts | 43 +
.../services/api/team-members-api.service.ts | 76 +
.../src/app/services/api/teams-api.service.ts | 57 +
.../services/api/test-gannt-api.service.ts | 31 +
.../app/services/api/timezones-api.service.ts | 27 +
.../app/services/api/todo-list-api.service.ts | 46 +
.../src/app/services/api/users.service.ts | 24 +
.../src/app/services/app.service.spec.ts | 16 +
.../src/app/services/app.service.ts | 63 +
.../src/app/services/auth.service.spec.ts | 16 +
.../src/app/services/auth.service.ts | 85 +
.../src/app/services/menu-service.spec.ts | 16 +
.../src/app/services/menu.service.ts | 36 +
.../notification-settings.service.spec.ts | 16 +
.../services/notification-settings.service.ts | 30 +
.../services/project-form-service.service.ts | 27 +
.../app/services/project-phases.service.ts | 48 +
.../app/services/project-template.service.ts | 48 +
.../app/services/project-updates.service.ts | 36 +
.../src/app/services/socket.service.ts | 35 +
.../src/app/services/utils.service.spec.ts | 16 +
.../src/app/services/utils.service.ts | 78 +
.../src/app/services/worklenz.analytics.ts | 66 +
.../session-expired.component.html | 22 +
.../session-expired.component.scss | 0
.../session-expired.component.spec.ts | 25 +
.../session-expired.component.ts | 33 +
worklenz-frontend/src/app/shared/constants.ts | 120 +
worklenz-frontend/src/app/shared/events.ts | 40 +
.../src/app/shared/session-helper.ts | 25 +
.../src/app/shared/socket-events.ts | 55 +
worklenz-frontend/src/app/shared/utils.ts | 120 +
.../app/shared/worklenz-analytics-events.ts | 157 +
worklenz-frontend/src/assets/.gitkeep | 0
.../src/assets/css/prebuilt-editor.css | 16 +
.../src/assets/icons/icon-128x128.png | Bin 0 -> 8396 bytes
.../src/assets/icons/icon-144x144.png | Bin 0 -> 9344 bytes
.../src/assets/icons/icon-152x152.png | Bin 0 -> 9823 bytes
.../src/assets/icons/icon-192x192.png | Bin 0 -> 12077 bytes
.../src/assets/icons/icon-384x384.png | Bin 0 -> 26068 bytes
.../src/assets/icons/icon-512x512.png | Bin 0 -> 29712 bytes
.../src/assets/icons/icon-72x72.png | Bin 0 -> 5410 bytes
.../src/assets/icons/icon-96x96.png | Bin 0 -> 6682 bytes
worklenz-frontend/src/assets/images/404.svg | 325 +
.../src/assets/images/block-user.png | Bin 0 -> 44000 bytes
.../src/assets/images/chevron-down-solid.svg | 4 +
.../src/assets/images/clipboard.png | Bin 0 -> 20743 bytes
.../src/assets/images/clock-green.png | Bin 0 -> 4981 bytes
.../src/assets/images/clock-red.png | Bin 0 -> 4922 bytes
.../src/assets/images/clock-yellow.png | Bin 0 -> 46382 bytes
.../src/assets/images/confetti (1).png | Bin 0 -> 20398 bytes
.../src/assets/images/confetti.png | Bin 0 -> 43336 bytes
.../src/assets/images/empty-box.png | Bin 0 -> 33801 bytes
.../src/assets/images/empty-box.webp | Bin 0 -> 19842 bytes
.../src/assets/images/files/ai.png | Bin 0 -> 10687 bytes
.../src/assets/images/files/avi.png | Bin 0 -> 7782 bytes
.../src/assets/images/files/css.png | Bin 0 -> 12761 bytes
.../src/assets/images/files/csv.png | Bin 0 -> 10053 bytes
.../src/assets/images/files/doc.png | Bin 0 -> 12689 bytes
.../src/assets/images/files/exe.png | Bin 0 -> 11570 bytes
.../src/assets/images/files/html.png | Bin 0 -> 6723 bytes
.../src/assets/images/files/jpg.png | Bin 0 -> 12146 bytes
.../src/assets/images/files/js.png | Bin 0 -> 11050 bytes
.../src/assets/images/files/json.png | Bin 0 -> 13972 bytes
.../src/assets/images/files/mp3.png | Bin 0 -> 13511 bytes
.../src/assets/images/files/mp4.png | Bin 0 -> 14697 bytes
.../src/assets/images/files/pdf.png | Bin 0 -> 13096 bytes
.../src/assets/images/files/png.png | Bin 0 -> 12544 bytes
.../src/assets/images/files/ppt.png | Bin 0 -> 6475 bytes
.../src/assets/images/files/psd.png | Bin 0 -> 12781 bytes
.../src/assets/images/files/search.png | Bin 0 -> 12353 bytes
.../src/assets/images/files/svg.png | Bin 0 -> 12957 bytes
.../src/assets/images/files/txt.png | Bin 0 -> 9600 bytes
.../src/assets/images/files/xls.png | Bin 0 -> 8267 bytes
.../src/assets/images/files/xml.png | Bin 0 -> 11677 bytes
.../src/assets/images/files/zip.png | Bin 0 -> 7376 bytes
.../src/assets/images/folder.svg | 5 +
.../src/assets/images/google-icon.png | Bin 0 -> 10275 bytes
.../src/assets/images/google_sign_in.png | Bin 0 -> 4308 bytes
.../src/assets/images/graph-report.png | Bin 0 -> 38597 bytes
worklenz-frontend/src/assets/images/group.png | Bin 0 -> 51416 bytes
.../src/assets/images/group_1.png | Bin 0 -> 49239 bytes
.../src/assets/images/insights-calendar.png | Bin 0 -> 19138 bytes
.../src/assets/images/insights-check.png | Bin 0 -> 24398 bytes
.../src/assets/images/logo-lg.png | Bin 0 -> 230748 bytes
.../src/assets/images/logo-sm.png | Bin 0 -> 4803 bytes
worklenz-frontend/src/assets/images/logo.png | Bin 0 -> 28062 bytes
.../assets/images/magnifying-glass-solid.svg | 5 +
.../src/assets/images/noimage.png | Bin 0 -> 15076 bytes
.../src/assets/images/open-icon.svg | 5 +
.../src/assets/images/progress.png | Bin 0 -> 24159 bytes
.../src/assets/images/project-management.png | Bin 0 -> 19755 bytes
.../src/assets/images/team-members.svg | 11 +
.../src/assets/images/to-do-list.png | Bin 0 -> 29396 bytes
worklenz-frontend/src/assets/images/user.png | Bin 0 -> 22004 bytes
.../src/assets/images/warning.png | Bin 0 -> 21073 bytes
.../src/environments/environment.prod.ts | 3 +
.../src/environments/environment.ts | 16 +
worklenz-frontend/src/favicon.ico | Bin 0 -> 11810 bytes
worklenz-frontend/src/index.html | 20 +
worklenz-frontend/src/main.ts | 12 +
worklenz-frontend/src/manifest.webmanifest | 59 +
.../src/res/js/jquery-3.6.1.min.js | 2 +
.../src/res/js/jquery.mentiony.js | 812 +
worklenz-frontend/src/res/js/smooth-scroll.js | 306 +
.../src/res/scss/antd-variables.scss | 41 +
worklenz-frontend/src/res/scss/antd.scss | 22275 ++++++++++++++++
worklenz-frontend/src/res/scss/gantt.scss | 407 +
.../src/scss/editable-table.scss | 73 +
worklenz-frontend/src/scss/scrollbar.scss | 29 +
worklenz-frontend/src/scss/task-view.scss | 54 +
worklenz-frontend/src/styles.scss | 1094 +
worklenz-frontend/src/test.ts | 11 +
worklenz-frontend/src/theme.less | 153 +
worklenz-frontend/src/typings.d.ts | 1 +
worklenz-frontend/tsconfig.app.json | 14 +
worklenz-frontend/tsconfig.json | 58 +
worklenz-frontend/tsconfig.spec.json | 17 +
3548 files changed, 193558 insertions(+), 3 deletions(-)
create mode 100644 worklenz-backend/.dockerignore
create mode 100644 worklenz-backend/.editorconfig
create mode 100644 worklenz-backend/.env.template
create mode 100644 worklenz-backend/.eslintrc.json
create mode 100644 worklenz-backend/.gitignore
create mode 100644 worklenz-backend/.gitmodules
create mode 100644 worklenz-backend/.npmrc
create mode 100644 worklenz-backend/Dockerfile
create mode 100644 worklenz-backend/Gruntfile.js
create mode 100644 worklenz-backend/README.md
create mode 100644 worklenz-backend/appspec.yml
create mode 100644 worklenz-backend/babel.config.js
create mode 100644 worklenz-backend/build.sh
create mode 100644 worklenz-backend/cli/esbuild-patch
create mode 100644 worklenz-backend/cli/generate-controller
create mode 100644 worklenz-backend/cli/generate-validator
create mode 100644 worklenz-backend/cli/inline-queries
create mode 100644 worklenz-backend/cli/mkrelease
create mode 100644 worklenz-backend/cli/swagger
create mode 100644 worklenz-backend/database/1_tables.sql
create mode 100644 worklenz-backend/database/2_triggers.sql
create mode 100644 worklenz-backend/database/3_system-data.sql
create mode 100644 worklenz-backend/database/4_views.sql
create mode 100644 worklenz-backend/database/5_functions.sql
create mode 100644 worklenz-backend/database/6_user-permission.sql
create mode 100644 worklenz-backend/database/README.md
create mode 100644 worklenz-backend/doc/Database.md
create mode 100644 worklenz-backend/esbuild.js
create mode 100644 worklenz-backend/grunt/grunt-compress.js
create mode 100644 worklenz-backend/jest.config.js
create mode 100644 worklenz-backend/new
create mode 100644 worklenz-backend/package-lock.json
create mode 100644 worklenz-backend/package.json
create mode 100644 worklenz-backend/release
create mode 100644 worklenz-backend/scss/style.scss
create mode 100644 worklenz-backend/sonar-project.properties
create mode 100644 worklenz-backend/src/app.ts
create mode 100644 worklenz-backend/src/bin/config.ts
create mode 100644 worklenz-backend/src/bin/www.ts
create mode 100644 worklenz-backend/src/config/db-config.ts
create mode 100644 worklenz-backend/src/config/db.ts
create mode 100644 worklenz-backend/src/controllers/.gitkeep
create mode 100644 worklenz-backend/src/controllers/access-controls-controller.ts
create mode 100644 worklenz-backend/src/controllers/activity-logs-controller.ts
create mode 100644 worklenz-backend/src/controllers/admin-center-controller.ts
create mode 100644 worklenz-backend/src/controllers/attachment-controller.ts
create mode 100644 worklenz-backend/src/controllers/auth-controller.ts
create mode 100644 worklenz-backend/src/controllers/aws-ses-controller.ts
create mode 100644 worklenz-backend/src/controllers/clients-controller.ts
create mode 100644 worklenz-backend/src/controllers/gantt-controller.ts
create mode 100644 worklenz-backend/src/controllers/home-page-controller.ts
create mode 100644 worklenz-backend/src/controllers/index-controller.ts
create mode 100644 worklenz-backend/src/controllers/job-titles-controller.ts
create mode 100644 worklenz-backend/src/controllers/labels-controller.ts
create mode 100644 worklenz-backend/src/controllers/logs-controller.ts
create mode 100644 worklenz-backend/src/controllers/notification-controller.ts
create mode 100644 worklenz-backend/src/controllers/overview-controller.ts
create mode 100644 worklenz-backend/src/controllers/personal-overview-controller.ts
create mode 100644 worklenz-backend/src/controllers/profile-settings-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-categories-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-comments-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-folders-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-healths-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-insights-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-managers-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-members-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-contoller-v2-base.ts
create mode 100644 worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-controller-v2.ts
create mode 100644 worklenz-backend/src/controllers/project-statuses-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/interfaces.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/project-templates-base.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/project-templates.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/pt-task-phases-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/pt-task-statuses-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/pt-tasks-controller-base.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-templates/pt-templates-controller.ts
create mode 100644 worklenz-backend/src/controllers/project-workload/workload-gannt-base.ts
create mode 100644 worklenz-backend/src/controllers/project-workload/workload-gannt-controller.ts
create mode 100644 worklenz-backend/src/controllers/projects-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/interfaces.ts
create mode 100644 worklenz-backend/src/controllers/reporting/overview/reporting-overview-base.ts
create mode 100644 worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/overview/reporting-overview-export-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/projects/reporting-projects-base.ts
create mode 100644 worklenz-backend/src/controllers/reporting/projects/reporting-projects-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/projects/reporting-projects-export-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/reporting-controller-base.ts
create mode 100644 worklenz-backend/src/controllers/reporting/reporting-info-controller.ts
create mode 100644 worklenz-backend/src/controllers/reporting/reporting-members-controller.ts
create mode 100644 worklenz-backend/src/controllers/resource-allocation-controller.ts
create mode 100644 worklenz-backend/src/controllers/schedule/schedule-controller-base.ts
create mode 100644 worklenz-backend/src/controllers/schedule/schedule-controller.ts
create mode 100644 worklenz-backend/src/controllers/shared-projects-controller.ts
create mode 100644 worklenz-backend/src/controllers/sub-tasks-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-comments-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-list-columns-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-phases-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-priorities-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-statuses-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-templates-controller.ts
create mode 100644 worklenz-backend/src/controllers/task-work-log-controller.ts
create mode 100644 worklenz-backend/src/controllers/tasks-controller-base.ts
create mode 100644 worklenz-backend/src/controllers/tasks-controller-v2.ts
create mode 100644 worklenz-backend/src/controllers/tasks-controller.ts
create mode 100644 worklenz-backend/src/controllers/tasks-custom-columns-controller.ts
create mode 100644 worklenz-backend/src/controllers/team-members-controller.ts
create mode 100644 worklenz-backend/src/controllers/teams-controller.ts
create mode 100644 worklenz-backend/src/controllers/timezones-controller.ts
create mode 100644 worklenz-backend/src/controllers/todo-list-controller.ts
create mode 100644 worklenz-backend/src/controllers/worklenz-controller-base.ts
create mode 100644 worklenz-backend/src/cron_jobs/daily-digest-job.ts
create mode 100644 worklenz-backend/src/cron_jobs/helpers.ts
create mode 100644 worklenz-backend/src/cron_jobs/index.ts
create mode 100644 worklenz-backend/src/cron_jobs/notifications-job.ts
create mode 100644 worklenz-backend/src/cron_jobs/project-digest-job.ts
create mode 100644 worklenz-backend/src/decorators/handle-exceptions.ts
create mode 100644 worklenz-backend/src/interfaces/.gitkeep
create mode 100644 worklenz-backend/src/interfaces/aws-bounced-email-response.ts
create mode 100644 worklenz-backend/src/interfaces/aws-complaint-email-response.ts
create mode 100644 worklenz-backend/src/interfaces/comment-email-notification.ts
create mode 100644 worklenz-backend/src/interfaces/daily-digest.ts
create mode 100644 worklenz-backend/src/interfaces/deserialize-callback.ts
create mode 100644 worklenz-backend/src/interfaces/email-template-type.ts
create mode 100644 worklenz-backend/src/interfaces/gantt-chart.ts
create mode 100644 worklenz-backend/src/interfaces/passport-session.ts
create mode 100644 worklenz-backend/src/interfaces/password-validity-result.ts
create mode 100644 worklenz-backend/src/interfaces/pg-session-data.ts
create mode 100644 worklenz-backend/src/interfaces/project-category.ts
create mode 100644 worklenz-backend/src/interfaces/project-digest.ts
create mode 100644 worklenz-backend/src/interfaces/project-folder.ts
create mode 100644 worklenz-backend/src/interfaces/serialize-callback.ts
create mode 100644 worklenz-backend/src/interfaces/socket-session.ts
create mode 100644 worklenz-backend/src/interfaces/task-assignments-model.ts
create mode 100644 worklenz-backend/src/interfaces/task-moved-to-done.ts
create mode 100644 worklenz-backend/src/interfaces/user.ts
create mode 100644 worklenz-backend/src/interfaces/worklenz-request.ts
create mode 100644 worklenz-backend/src/interfaces/worklenz-response.ts
create mode 100644 worklenz-backend/src/json_schemas/email-request-schema.ts
create mode 100644 worklenz-backend/src/json_schemas/item-create-schema.ts
create mode 100644 worklenz-backend/src/json_schemas/project-category-schema.ts
create mode 100644 worklenz-backend/src/json_schemas/project-folder-schema.ts
create mode 100644 worklenz-backend/src/json_schemas/task-phase-create-schema.ts
create mode 100644 worklenz-backend/src/middlewares/image-to-webp.ts
create mode 100644 worklenz-backend/src/middlewares/licensing-middleware.ts
create mode 100644 worklenz-backend/src/middlewares/map-tasks-to-bulk-update.ts
create mode 100644 worklenz-backend/src/middlewares/schema-validator.ts
create mode 100644 worklenz-backend/src/middlewares/session-middleware.ts
create mode 100644 worklenz-backend/src/middlewares/validators/README.md
create mode 100644 worklenz-backend/src/middlewares/validators/avatar-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/body-name-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/bulk-tasks-phase-validators.ts
create mode 100644 worklenz-backend/src/middlewares/validators/bulk-tasks-priority-validators.ts
create mode 100644 worklenz-backend/src/middlewares/validators/bulk-tasks-status-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/bulk-tasks-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/clients-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/gantt-tasks-query-params-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/gantt-tasks-range-params-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/home-task-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/id-param-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/import-task-templates-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/job-titles-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/kanban-status-update-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/organization-owner-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/organization-settings-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/password-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/profile-settings-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/project-manager-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/project-member-invite-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/project-member-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/projects-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/pt-task-status-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/quick-task-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/reset-email-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/setup-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/shared-projects-create-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/sign-up-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/status-delete-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/status-order-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/task-attachments-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/task-comment-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/task-phase-name-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/task-status-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/task-time-log-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/tasks-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/team-members-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/team-owner-or-admin-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/team-settings-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/teams-activate-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/todo-list-body-validator.ts
create mode 100644 worklenz-backend/src/middlewares/validators/update-password-validator.ts
create mode 100644 worklenz-backend/src/middlewares/verify-project-membership.ts
create mode 100644 worklenz-backend/src/migrations/.gitkeep
create mode 100644 worklenz-backend/src/migrations/add-projects-keys.ts
create mode 100644 worklenz-backend/src/models/.gitkeep
create mode 100644 worklenz-backend/src/models/auth-response.ts
create mode 100644 worklenz-backend/src/models/reporting-export.ts
create mode 100644 worklenz-backend/src/models/server-response.ts
create mode 100644 worklenz-backend/src/passport/deserialize.ts
create mode 100644 worklenz-backend/src/passport/index.ts
create mode 100644 worklenz-backend/src/passport/passport-strategies/passport-constants.ts
create mode 100644 worklenz-backend/src/passport/passport-strategies/passport-google.ts
create mode 100644 worklenz-backend/src/passport/passport-strategies/passport-local-login.ts
create mode 100644 worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts
create mode 100644 worklenz-backend/src/passport/serialize.ts
create mode 100644 worklenz-backend/src/pg_notify_listeners/db-task-status-changed.ts
create mode 100644 worklenz-backend/src/public/140.090f8f66847cac61.js
create mode 100644 worklenz-backend/src/public/148.3562c20f7c79dfbe.js
create mode 100644 worklenz-backend/src/public/150.aea42238d373936d.js
create mode 100644 worklenz-backend/src/public/152.89824b7edf9e2b3f.js
create mode 100644 worklenz-backend/src/public/169.3681839fd43f684a.js
create mode 100644 worklenz-backend/src/public/178.4e9f4f897f5f7737.js
create mode 100644 worklenz-backend/src/public/215.ab5a0edadc383dc1.js
create mode 100644 worklenz-backend/src/public/217.3eaf97d62e624659.js
create mode 100644 worklenz-backend/src/public/226.342b896c33d8d900.js
create mode 100644 worklenz-backend/src/public/265.e0fb54844d977984.js
create mode 100644 worklenz-backend/src/public/269.c4d92a972c5787a6.js
create mode 100644 worklenz-backend/src/public/31.366d3b5b3a40bd28.js
create mode 100644 worklenz-backend/src/public/3rdpartylicenses.txt
create mode 100644 worklenz-backend/src/public/406.ffcf05b3e5e7445d.js
create mode 100644 worklenz-backend/src/public/422.7a1aad3ba048190e.js
create mode 100644 worklenz-backend/src/public/450.2aa94132069852f3.js
create mode 100644 worklenz-backend/src/public/453.850082f04ceb33a4.js
create mode 100644 worklenz-backend/src/public/457.e5352d17b3d4ace6.js
create mode 100644 worklenz-backend/src/public/508.695e49a71f5042aa.js
create mode 100644 worklenz-backend/src/public/526.31f0f862d4cc0de0.js
create mode 100644 worklenz-backend/src/public/554.74cb9e7b0d19662d.js
create mode 100644 worklenz-backend/src/public/570.7a7379f081e3b4a7.js
create mode 100644 worklenz-backend/src/public/589.fc04e75bdb5d1681.js
create mode 100644 worklenz-backend/src/public/596.122034b2d91e32f1.js
create mode 100644 worklenz-backend/src/public/693.e0ca38056f3a2978.js
create mode 100644 worklenz-backend/src/public/771.c1c131496d7c6ea4.js
create mode 100644 worklenz-backend/src/public/787.c0da7f19a032adb5.js
create mode 100644 worklenz-backend/src/public/889.45a538df588b6347.js
create mode 100644 worklenz-backend/src/public/89.056b6bfe2710d324.js
create mode 100644 worklenz-backend/src/public/896.ca94baf9db3b3b4c.js
create mode 100644 worklenz-backend/src/public/921.09750d890ab4e8d5.js
create mode 100644 worklenz-backend/src/public/931.330457ea4270cc95.js
create mode 100644 worklenz-backend/src/public/940.48769e1469dae2f9.js
create mode 100644 worklenz-backend/src/public/945.9a0115568762948e.js
create mode 100644 worklenz-backend/src/public/assets/animal/panda.js
create mode 100644 worklenz-backend/src/public/assets/animal/panda.svg
create mode 100644 worklenz-backend/src/public/assets/css/prebuilt-editor.css
create mode 100644 worklenz-backend/src/public/assets/fill/.gitkeep
create mode 100644 worklenz-backend/src/public/assets/fill/account-book.js
create mode 100644 worklenz-backend/src/public/assets/fill/account-book.svg
create mode 100644 worklenz-backend/src/public/assets/fill/alert.js
create mode 100644 worklenz-backend/src/public/assets/fill/alert.svg
create mode 100644 worklenz-backend/src/public/assets/fill/alipay-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/alipay-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/alipay-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/alipay-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/aliwangwang.js
create mode 100644 worklenz-backend/src/public/assets/fill/aliwangwang.svg
create mode 100644 worklenz-backend/src/public/assets/fill/amazon-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/amazon-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/amazon-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/amazon-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/android.js
create mode 100644 worklenz-backend/src/public/assets/fill/android.svg
create mode 100644 worklenz-backend/src/public/assets/fill/api.js
create mode 100644 worklenz-backend/src/public/assets/fill/api.svg
create mode 100644 worklenz-backend/src/public/assets/fill/apple.js
create mode 100644 worklenz-backend/src/public/assets/fill/apple.svg
create mode 100644 worklenz-backend/src/public/assets/fill/appstore.js
create mode 100644 worklenz-backend/src/public/assets/fill/appstore.svg
create mode 100644 worklenz-backend/src/public/assets/fill/audio.js
create mode 100644 worklenz-backend/src/public/assets/fill/audio.svg
create mode 100644 worklenz-backend/src/public/assets/fill/backward.js
create mode 100644 worklenz-backend/src/public/assets/fill/backward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/bank.js
create mode 100644 worklenz-backend/src/public/assets/fill/bank.svg
create mode 100644 worklenz-backend/src/public/assets/fill/behance-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/behance-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/behance-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/behance-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/bell.js
create mode 100644 worklenz-backend/src/public/assets/fill/bell.svg
create mode 100644 worklenz-backend/src/public/assets/fill/book.js
create mode 100644 worklenz-backend/src/public/assets/fill/book.svg
create mode 100644 worklenz-backend/src/public/assets/fill/box-plot.js
create mode 100644 worklenz-backend/src/public/assets/fill/box-plot.svg
create mode 100644 worklenz-backend/src/public/assets/fill/bug.js
create mode 100644 worklenz-backend/src/public/assets/fill/bug.svg
create mode 100644 worklenz-backend/src/public/assets/fill/build.js
create mode 100644 worklenz-backend/src/public/assets/fill/build.svg
create mode 100644 worklenz-backend/src/public/assets/fill/bulb.js
create mode 100644 worklenz-backend/src/public/assets/fill/bulb.svg
create mode 100644 worklenz-backend/src/public/assets/fill/calculator.js
create mode 100644 worklenz-backend/src/public/assets/fill/calculator.svg
create mode 100644 worklenz-backend/src/public/assets/fill/calendar.js
create mode 100644 worklenz-backend/src/public/assets/fill/calendar.svg
create mode 100644 worklenz-backend/src/public/assets/fill/camera.js
create mode 100644 worklenz-backend/src/public/assets/fill/camera.svg
create mode 100644 worklenz-backend/src/public/assets/fill/car.js
create mode 100644 worklenz-backend/src/public/assets/fill/car.svg
create mode 100644 worklenz-backend/src/public/assets/fill/caret-down.js
create mode 100644 worklenz-backend/src/public/assets/fill/caret-down.svg
create mode 100644 worklenz-backend/src/public/assets/fill/caret-left.js
create mode 100644 worklenz-backend/src/public/assets/fill/caret-left.svg
create mode 100644 worklenz-backend/src/public/assets/fill/caret-right.js
create mode 100644 worklenz-backend/src/public/assets/fill/caret-right.svg
create mode 100644 worklenz-backend/src/public/assets/fill/caret-up.js
create mode 100644 worklenz-backend/src/public/assets/fill/caret-up.svg
create mode 100644 worklenz-backend/src/public/assets/fill/carry-out.js
create mode 100644 worklenz-backend/src/public/assets/fill/carry-out.svg
create mode 100644 worklenz-backend/src/public/assets/fill/check-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/check-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/check-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/check-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/chrome.js
create mode 100644 worklenz-backend/src/public/assets/fill/chrome.svg
create mode 100644 worklenz-backend/src/public/assets/fill/ci-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/ci-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/clock-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/clock-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/close-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/close-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/close-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/close-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/cloud.js
create mode 100644 worklenz-backend/src/public/assets/fill/cloud.svg
create mode 100644 worklenz-backend/src/public/assets/fill/code-sandbox-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/code-sandbox-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/code-sandbox-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/code-sandbox-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/code.js
create mode 100644 worklenz-backend/src/public/assets/fill/code.svg
create mode 100644 worklenz-backend/src/public/assets/fill/codepen-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/codepen-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/codepen-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/codepen-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/compass.js
create mode 100644 worklenz-backend/src/public/assets/fill/compass.svg
create mode 100644 worklenz-backend/src/public/assets/fill/contacts.js
create mode 100644 worklenz-backend/src/public/assets/fill/contacts.svg
create mode 100644 worklenz-backend/src/public/assets/fill/container.js
create mode 100644 worklenz-backend/src/public/assets/fill/container.svg
create mode 100644 worklenz-backend/src/public/assets/fill/control.js
create mode 100644 worklenz-backend/src/public/assets/fill/control.svg
create mode 100644 worklenz-backend/src/public/assets/fill/copy.js
create mode 100644 worklenz-backend/src/public/assets/fill/copy.svg
create mode 100644 worklenz-backend/src/public/assets/fill/copyright-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/copyright-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/credit-card.js
create mode 100644 worklenz-backend/src/public/assets/fill/credit-card.svg
create mode 100644 worklenz-backend/src/public/assets/fill/crown.js
create mode 100644 worklenz-backend/src/public/assets/fill/crown.svg
create mode 100644 worklenz-backend/src/public/assets/fill/customer-service.js
create mode 100644 worklenz-backend/src/public/assets/fill/customer-service.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dashboard.js
create mode 100644 worklenz-backend/src/public/assets/fill/dashboard.svg
create mode 100644 worklenz-backend/src/public/assets/fill/database.js
create mode 100644 worklenz-backend/src/public/assets/fill/database.svg
create mode 100644 worklenz-backend/src/public/assets/fill/delete.js
create mode 100644 worklenz-backend/src/public/assets/fill/delete.svg
create mode 100644 worklenz-backend/src/public/assets/fill/diff.js
create mode 100644 worklenz-backend/src/public/assets/fill/diff.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dingtalk-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/dingtalk-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dingtalk-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/dingtalk-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dislike.js
create mode 100644 worklenz-backend/src/public/assets/fill/dislike.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dollar-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/dollar-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/down-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/down-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/down-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/down-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dribbble-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/dribbble-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dribbble-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/dribbble-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dropbox-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/dropbox-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/dropbox-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/dropbox-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/edit.js
create mode 100644 worklenz-backend/src/public/assets/fill/edit.svg
create mode 100644 worklenz-backend/src/public/assets/fill/environment.js
create mode 100644 worklenz-backend/src/public/assets/fill/environment.svg
create mode 100644 worklenz-backend/src/public/assets/fill/euro-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/euro-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/exclamation-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/exclamation-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/experiment.js
create mode 100644 worklenz-backend/src/public/assets/fill/experiment.svg
create mode 100644 worklenz-backend/src/public/assets/fill/eye-invisible.js
create mode 100644 worklenz-backend/src/public/assets/fill/eye-invisible.svg
create mode 100644 worklenz-backend/src/public/assets/fill/eye.js
create mode 100644 worklenz-backend/src/public/assets/fill/eye.svg
create mode 100644 worklenz-backend/src/public/assets/fill/facebook.js
create mode 100644 worklenz-backend/src/public/assets/fill/facebook.svg
create mode 100644 worklenz-backend/src/public/assets/fill/fast-backward.js
create mode 100644 worklenz-backend/src/public/assets/fill/fast-backward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/fast-forward.js
create mode 100644 worklenz-backend/src/public/assets/fill/fast-forward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-add.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-add.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-excel.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-excel.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-exclamation.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-exclamation.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-image.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-image.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-markdown.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-markdown.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-pdf.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-pdf.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-ppt.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-ppt.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-text.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-text.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-unknown.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-unknown.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-word.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-word.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file-zip.js
create mode 100644 worklenz-backend/src/public/assets/fill/file-zip.svg
create mode 100644 worklenz-backend/src/public/assets/fill/file.js
create mode 100644 worklenz-backend/src/public/assets/fill/file.svg
create mode 100644 worklenz-backend/src/public/assets/fill/filter.js
create mode 100644 worklenz-backend/src/public/assets/fill/filter.svg
create mode 100644 worklenz-backend/src/public/assets/fill/fire.js
create mode 100644 worklenz-backend/src/public/assets/fill/fire.svg
create mode 100644 worklenz-backend/src/public/assets/fill/flag.js
create mode 100644 worklenz-backend/src/public/assets/fill/flag.svg
create mode 100644 worklenz-backend/src/public/assets/fill/folder-add.js
create mode 100644 worklenz-backend/src/public/assets/fill/folder-add.svg
create mode 100644 worklenz-backend/src/public/assets/fill/folder-open.js
create mode 100644 worklenz-backend/src/public/assets/fill/folder-open.svg
create mode 100644 worklenz-backend/src/public/assets/fill/folder.js
create mode 100644 worklenz-backend/src/public/assets/fill/folder.svg
create mode 100644 worklenz-backend/src/public/assets/fill/format-painter.js
create mode 100644 worklenz-backend/src/public/assets/fill/format-painter.svg
create mode 100644 worklenz-backend/src/public/assets/fill/forward.js
create mode 100644 worklenz-backend/src/public/assets/fill/forward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/frown.js
create mode 100644 worklenz-backend/src/public/assets/fill/frown.svg
create mode 100644 worklenz-backend/src/public/assets/fill/fund.js
create mode 100644 worklenz-backend/src/public/assets/fill/fund.svg
create mode 100644 worklenz-backend/src/public/assets/fill/funnel-plot.js
create mode 100644 worklenz-backend/src/public/assets/fill/funnel-plot.svg
create mode 100644 worklenz-backend/src/public/assets/fill/gift.js
create mode 100644 worklenz-backend/src/public/assets/fill/gift.svg
create mode 100644 worklenz-backend/src/public/assets/fill/github.js
create mode 100644 worklenz-backend/src/public/assets/fill/github.svg
create mode 100644 worklenz-backend/src/public/assets/fill/gitlab.js
create mode 100644 worklenz-backend/src/public/assets/fill/gitlab.svg
create mode 100644 worklenz-backend/src/public/assets/fill/gold.js
create mode 100644 worklenz-backend/src/public/assets/fill/gold.svg
create mode 100644 worklenz-backend/src/public/assets/fill/golden.js
create mode 100644 worklenz-backend/src/public/assets/fill/golden.svg
create mode 100644 worklenz-backend/src/public/assets/fill/google-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/google-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/google-plus-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/google-plus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/google-plus-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/google-plus-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/google-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/google-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/hdd.js
create mode 100644 worklenz-backend/src/public/assets/fill/hdd.svg
create mode 100644 worklenz-backend/src/public/assets/fill/heart.js
create mode 100644 worklenz-backend/src/public/assets/fill/heart.svg
create mode 100644 worklenz-backend/src/public/assets/fill/highlight.js
create mode 100644 worklenz-backend/src/public/assets/fill/highlight.svg
create mode 100644 worklenz-backend/src/public/assets/fill/home.js
create mode 100644 worklenz-backend/src/public/assets/fill/home.svg
create mode 100644 worklenz-backend/src/public/assets/fill/hourglass.js
create mode 100644 worklenz-backend/src/public/assets/fill/hourglass.svg
create mode 100644 worklenz-backend/src/public/assets/fill/html5.js
create mode 100644 worklenz-backend/src/public/assets/fill/html5.svg
create mode 100644 worklenz-backend/src/public/assets/fill/idcard.js
create mode 100644 worklenz-backend/src/public/assets/fill/idcard.svg
create mode 100644 worklenz-backend/src/public/assets/fill/ie-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/ie-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/ie-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/ie-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/info-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/info-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/instagram.js
create mode 100644 worklenz-backend/src/public/assets/fill/instagram.svg
create mode 100644 worklenz-backend/src/public/assets/fill/insurance.js
create mode 100644 worklenz-backend/src/public/assets/fill/insurance.svg
create mode 100644 worklenz-backend/src/public/assets/fill/interaction.js
create mode 100644 worklenz-backend/src/public/assets/fill/interaction.svg
create mode 100644 worklenz-backend/src/public/assets/fill/layout.js
create mode 100644 worklenz-backend/src/public/assets/fill/layout.svg
create mode 100644 worklenz-backend/src/public/assets/fill/left-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/left-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/left-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/left-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/like.js
create mode 100644 worklenz-backend/src/public/assets/fill/like.svg
create mode 100644 worklenz-backend/src/public/assets/fill/linkedin.js
create mode 100644 worklenz-backend/src/public/assets/fill/linkedin.svg
create mode 100644 worklenz-backend/src/public/assets/fill/lock.js
create mode 100644 worklenz-backend/src/public/assets/fill/lock.svg
create mode 100644 worklenz-backend/src/public/assets/fill/mac-command.js
create mode 100644 worklenz-backend/src/public/assets/fill/mac-command.svg
create mode 100644 worklenz-backend/src/public/assets/fill/mail.js
create mode 100644 worklenz-backend/src/public/assets/fill/mail.svg
create mode 100644 worklenz-backend/src/public/assets/fill/medicine-box.js
create mode 100644 worklenz-backend/src/public/assets/fill/medicine-box.svg
create mode 100644 worklenz-backend/src/public/assets/fill/medium-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/medium-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/medium-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/medium-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/meh.js
create mode 100644 worklenz-backend/src/public/assets/fill/meh.svg
create mode 100644 worklenz-backend/src/public/assets/fill/message.js
create mode 100644 worklenz-backend/src/public/assets/fill/message.svg
create mode 100644 worklenz-backend/src/public/assets/fill/minus-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/minus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/minus-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/minus-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/mobile.js
create mode 100644 worklenz-backend/src/public/assets/fill/mobile.svg
create mode 100644 worklenz-backend/src/public/assets/fill/money-collect.js
create mode 100644 worklenz-backend/src/public/assets/fill/money-collect.svg
create mode 100644 worklenz-backend/src/public/assets/fill/notification.js
create mode 100644 worklenz-backend/src/public/assets/fill/notification.svg
create mode 100644 worklenz-backend/src/public/assets/fill/pause-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/pause-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/pay-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/pay-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/phone.js
create mode 100644 worklenz-backend/src/public/assets/fill/phone.svg
create mode 100644 worklenz-backend/src/public/assets/fill/picture.js
create mode 100644 worklenz-backend/src/public/assets/fill/picture.svg
create mode 100644 worklenz-backend/src/public/assets/fill/pie-chart.js
create mode 100644 worklenz-backend/src/public/assets/fill/pie-chart.svg
create mode 100644 worklenz-backend/src/public/assets/fill/play-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/play-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/play-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/play-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/plus-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/plus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/plus-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/plus-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/pound-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/pound-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/printer.js
create mode 100644 worklenz-backend/src/public/assets/fill/printer.svg
create mode 100644 worklenz-backend/src/public/assets/fill/profile.js
create mode 100644 worklenz-backend/src/public/assets/fill/profile.svg
create mode 100644 worklenz-backend/src/public/assets/fill/project.js
create mode 100644 worklenz-backend/src/public/assets/fill/project.svg
create mode 100644 worklenz-backend/src/public/assets/fill/property-safety.js
create mode 100644 worklenz-backend/src/public/assets/fill/property-safety.svg
create mode 100644 worklenz-backend/src/public/assets/fill/pushpin.js
create mode 100644 worklenz-backend/src/public/assets/fill/pushpin.svg
create mode 100644 worklenz-backend/src/public/assets/fill/qq-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/qq-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/qq-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/qq-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/question-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/question-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/read.js
create mode 100644 worklenz-backend/src/public/assets/fill/read.svg
create mode 100644 worklenz-backend/src/public/assets/fill/reconciliation.js
create mode 100644 worklenz-backend/src/public/assets/fill/reconciliation.svg
create mode 100644 worklenz-backend/src/public/assets/fill/red-envelope.js
create mode 100644 worklenz-backend/src/public/assets/fill/red-envelope.svg
create mode 100644 worklenz-backend/src/public/assets/fill/reddit-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/reddit-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/reddit-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/reddit-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/rest.js
create mode 100644 worklenz-backend/src/public/assets/fill/rest.svg
create mode 100644 worklenz-backend/src/public/assets/fill/right-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/right-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/right-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/right-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/robot.js
create mode 100644 worklenz-backend/src/public/assets/fill/robot.svg
create mode 100644 worklenz-backend/src/public/assets/fill/rocket.js
create mode 100644 worklenz-backend/src/public/assets/fill/rocket.svg
create mode 100644 worklenz-backend/src/public/assets/fill/safety-certificate.js
create mode 100644 worklenz-backend/src/public/assets/fill/safety-certificate.svg
create mode 100644 worklenz-backend/src/public/assets/fill/save.js
create mode 100644 worklenz-backend/src/public/assets/fill/save.svg
create mode 100644 worklenz-backend/src/public/assets/fill/schedule.js
create mode 100644 worklenz-backend/src/public/assets/fill/schedule.svg
create mode 100644 worklenz-backend/src/public/assets/fill/security-scan.js
create mode 100644 worklenz-backend/src/public/assets/fill/security-scan.svg
create mode 100644 worklenz-backend/src/public/assets/fill/setting.js
create mode 100644 worklenz-backend/src/public/assets/fill/setting.svg
create mode 100644 worklenz-backend/src/public/assets/fill/shop.js
create mode 100644 worklenz-backend/src/public/assets/fill/shop.svg
create mode 100644 worklenz-backend/src/public/assets/fill/shopping.js
create mode 100644 worklenz-backend/src/public/assets/fill/shopping.svg
create mode 100644 worklenz-backend/src/public/assets/fill/signal.js
create mode 100644 worklenz-backend/src/public/assets/fill/signal.svg
create mode 100644 worklenz-backend/src/public/assets/fill/sketch-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/sketch-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/sketch-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/sketch-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/skin.js
create mode 100644 worklenz-backend/src/public/assets/fill/skin.svg
create mode 100644 worklenz-backend/src/public/assets/fill/skype.js
create mode 100644 worklenz-backend/src/public/assets/fill/skype.svg
create mode 100644 worklenz-backend/src/public/assets/fill/slack-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/slack-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/slack-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/slack-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/sliders.js
create mode 100644 worklenz-backend/src/public/assets/fill/sliders.svg
create mode 100644 worklenz-backend/src/public/assets/fill/smile.js
create mode 100644 worklenz-backend/src/public/assets/fill/smile.svg
create mode 100644 worklenz-backend/src/public/assets/fill/snippets.js
create mode 100644 worklenz-backend/src/public/assets/fill/snippets.svg
create mode 100644 worklenz-backend/src/public/assets/fill/sound.js
create mode 100644 worklenz-backend/src/public/assets/fill/sound.svg
create mode 100644 worklenz-backend/src/public/assets/fill/star.js
create mode 100644 worklenz-backend/src/public/assets/fill/star.svg
create mode 100644 worklenz-backend/src/public/assets/fill/step-backward.js
create mode 100644 worklenz-backend/src/public/assets/fill/step-backward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/step-forward.js
create mode 100644 worklenz-backend/src/public/assets/fill/step-forward.svg
create mode 100644 worklenz-backend/src/public/assets/fill/stop.js
create mode 100644 worklenz-backend/src/public/assets/fill/stop.svg
create mode 100644 worklenz-backend/src/public/assets/fill/switcher.js
create mode 100644 worklenz-backend/src/public/assets/fill/switcher.svg
create mode 100644 worklenz-backend/src/public/assets/fill/tablet.js
create mode 100644 worklenz-backend/src/public/assets/fill/tablet.svg
create mode 100644 worklenz-backend/src/public/assets/fill/tag.js
create mode 100644 worklenz-backend/src/public/assets/fill/tag.svg
create mode 100644 worklenz-backend/src/public/assets/fill/tags.js
create mode 100644 worklenz-backend/src/public/assets/fill/tags.svg
create mode 100644 worklenz-backend/src/public/assets/fill/taobao-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/taobao-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/taobao-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/taobao-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/thunderbolt.js
create mode 100644 worklenz-backend/src/public/assets/fill/thunderbolt.svg
create mode 100644 worklenz-backend/src/public/assets/fill/tool.js
create mode 100644 worklenz-backend/src/public/assets/fill/tool.svg
create mode 100644 worklenz-backend/src/public/assets/fill/trademark-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/trademark-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/trophy.js
create mode 100644 worklenz-backend/src/public/assets/fill/trophy.svg
create mode 100644 worklenz-backend/src/public/assets/fill/twitter-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/twitter-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/twitter-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/twitter-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/unlock.js
create mode 100644 worklenz-backend/src/public/assets/fill/unlock.svg
create mode 100644 worklenz-backend/src/public/assets/fill/up-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/up-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/up-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/up-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/usb.js
create mode 100644 worklenz-backend/src/public/assets/fill/usb.svg
create mode 100644 worklenz-backend/src/public/assets/fill/video-camera.js
create mode 100644 worklenz-backend/src/public/assets/fill/video-camera.svg
create mode 100644 worklenz-backend/src/public/assets/fill/wallet.js
create mode 100644 worklenz-backend/src/public/assets/fill/wallet.svg
create mode 100644 worklenz-backend/src/public/assets/fill/warning.js
create mode 100644 worklenz-backend/src/public/assets/fill/warning.svg
create mode 100644 worklenz-backend/src/public/assets/fill/wechat.js
create mode 100644 worklenz-backend/src/public/assets/fill/wechat.svg
create mode 100644 worklenz-backend/src/public/assets/fill/weibo-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/weibo-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/weibo-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/weibo-square.svg
create mode 100644 worklenz-backend/src/public/assets/fill/windows.js
create mode 100644 worklenz-backend/src/public/assets/fill/windows.svg
create mode 100644 worklenz-backend/src/public/assets/fill/yahoo.js
create mode 100644 worklenz-backend/src/public/assets/fill/yahoo.svg
create mode 100644 worklenz-backend/src/public/assets/fill/youtube.js
create mode 100644 worklenz-backend/src/public/assets/fill/youtube.svg
create mode 100644 worklenz-backend/src/public/assets/fill/yuque.js
create mode 100644 worklenz-backend/src/public/assets/fill/yuque.svg
create mode 100644 worklenz-backend/src/public/assets/fill/zhihu-circle.js
create mode 100644 worklenz-backend/src/public/assets/fill/zhihu-circle.svg
create mode 100644 worklenz-backend/src/public/assets/fill/zhihu-square.js
create mode 100644 worklenz-backend/src/public/assets/fill/zhihu-square.svg
create mode 100644 worklenz-backend/src/public/assets/icons/icon-128x128.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-144x144.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-152x152.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-192x192.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-384x384.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-512x512.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-72x72.png
create mode 100644 worklenz-backend/src/public/assets/icons/icon-96x96.png
create mode 100644 worklenz-backend/src/public/assets/images/404.svg
create mode 100644 worklenz-backend/src/public/assets/images/block-user.png
create mode 100644 worklenz-backend/src/public/assets/images/chevron-down-solid.svg
create mode 100644 worklenz-backend/src/public/assets/images/clipboard.png
create mode 100644 worklenz-backend/src/public/assets/images/clock-green.png
create mode 100644 worklenz-backend/src/public/assets/images/clock-red.png
create mode 100644 worklenz-backend/src/public/assets/images/clock-yellow.png
create mode 100644 worklenz-backend/src/public/assets/images/confetti (1).png
create mode 100644 worklenz-backend/src/public/assets/images/confetti.png
create mode 100644 worklenz-backend/src/public/assets/images/empty-box.png
create mode 100644 worklenz-backend/src/public/assets/images/empty-box.webp
create mode 100644 worklenz-backend/src/public/assets/images/files/ai.png
create mode 100644 worklenz-backend/src/public/assets/images/files/avi.png
create mode 100644 worklenz-backend/src/public/assets/images/files/css.png
create mode 100644 worklenz-backend/src/public/assets/images/files/csv.png
create mode 100644 worklenz-backend/src/public/assets/images/files/doc.png
create mode 100644 worklenz-backend/src/public/assets/images/files/exe.png
create mode 100644 worklenz-backend/src/public/assets/images/files/html.png
create mode 100644 worklenz-backend/src/public/assets/images/files/jpg.png
create mode 100644 worklenz-backend/src/public/assets/images/files/js.png
create mode 100644 worklenz-backend/src/public/assets/images/files/json.png
create mode 100644 worklenz-backend/src/public/assets/images/files/mp3.png
create mode 100644 worklenz-backend/src/public/assets/images/files/mp4.png
create mode 100644 worklenz-backend/src/public/assets/images/files/pdf.png
create mode 100644 worklenz-backend/src/public/assets/images/files/png.png
create mode 100644 worklenz-backend/src/public/assets/images/files/ppt.png
create mode 100644 worklenz-backend/src/public/assets/images/files/psd.png
create mode 100644 worklenz-backend/src/public/assets/images/files/search.png
create mode 100644 worklenz-backend/src/public/assets/images/files/svg.png
create mode 100644 worklenz-backend/src/public/assets/images/files/txt.png
create mode 100644 worklenz-backend/src/public/assets/images/files/xls.png
create mode 100644 worklenz-backend/src/public/assets/images/files/xml.png
create mode 100644 worklenz-backend/src/public/assets/images/files/zip.png
create mode 100644 worklenz-backend/src/public/assets/images/folder.svg
create mode 100644 worklenz-backend/src/public/assets/images/google-icon.png
create mode 100644 worklenz-backend/src/public/assets/images/google_sign_in.png
create mode 100644 worklenz-backend/src/public/assets/images/graph-report.png
create mode 100644 worklenz-backend/src/public/assets/images/group.png
create mode 100644 worklenz-backend/src/public/assets/images/group_1.png
create mode 100644 worklenz-backend/src/public/assets/images/insights-calendar.png
create mode 100644 worklenz-backend/src/public/assets/images/insights-check.png
create mode 100644 worklenz-backend/src/public/assets/images/logo-lg.png
create mode 100644 worklenz-backend/src/public/assets/images/logo-sm.png
create mode 100644 worklenz-backend/src/public/assets/images/logo.png
create mode 100644 worklenz-backend/src/public/assets/images/magnifying-glass-solid.svg
create mode 100644 worklenz-backend/src/public/assets/images/noimage.png
create mode 100644 worklenz-backend/src/public/assets/images/open-icon.svg
create mode 100644 worklenz-backend/src/public/assets/images/progress.png
create mode 100644 worklenz-backend/src/public/assets/images/project-management.png
create mode 100644 worklenz-backend/src/public/assets/images/team-members.svg
create mode 100644 worklenz-backend/src/public/assets/images/to-do-list.png
create mode 100644 worklenz-backend/src/public/assets/images/user.png
create mode 100644 worklenz-backend/src/public/assets/images/warning.png
create mode 100644 worklenz-backend/src/public/assets/outline/.gitkeep
create mode 100644 worklenz-backend/src/public/assets/outline/account-book.js
create mode 100644 worklenz-backend/src/public/assets/outline/account-book.svg
create mode 100644 worklenz-backend/src/public/assets/outline/aim.js
create mode 100644 worklenz-backend/src/public/assets/outline/aim.svg
create mode 100644 worklenz-backend/src/public/assets/outline/alert.js
create mode 100644 worklenz-backend/src/public/assets/outline/alert.svg
create mode 100644 worklenz-backend/src/public/assets/outline/alibaba.js
create mode 100644 worklenz-backend/src/public/assets/outline/alibaba.svg
create mode 100644 worklenz-backend/src/public/assets/outline/align-center.js
create mode 100644 worklenz-backend/src/public/assets/outline/align-center.svg
create mode 100644 worklenz-backend/src/public/assets/outline/align-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/align-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/align-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/align-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/alipay-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/alipay-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/alipay.js
create mode 100644 worklenz-backend/src/public/assets/outline/alipay.svg
create mode 100644 worklenz-backend/src/public/assets/outline/aliwangwang.js
create mode 100644 worklenz-backend/src/public/assets/outline/aliwangwang.svg
create mode 100644 worklenz-backend/src/public/assets/outline/aliyun.js
create mode 100644 worklenz-backend/src/public/assets/outline/aliyun.svg
create mode 100644 worklenz-backend/src/public/assets/outline/amazon.js
create mode 100644 worklenz-backend/src/public/assets/outline/amazon.svg
create mode 100644 worklenz-backend/src/public/assets/outline/android.js
create mode 100644 worklenz-backend/src/public/assets/outline/android.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ant-cloud.js
create mode 100644 worklenz-backend/src/public/assets/outline/ant-cloud.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ant-design.js
create mode 100644 worklenz-backend/src/public/assets/outline/ant-design.svg
create mode 100644 worklenz-backend/src/public/assets/outline/apartment.js
create mode 100644 worklenz-backend/src/public/assets/outline/apartment.svg
create mode 100644 worklenz-backend/src/public/assets/outline/api.js
create mode 100644 worklenz-backend/src/public/assets/outline/api.svg
create mode 100644 worklenz-backend/src/public/assets/outline/apple.js
create mode 100644 worklenz-backend/src/public/assets/outline/apple.svg
create mode 100644 worklenz-backend/src/public/assets/outline/appstore-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/appstore-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/appstore.js
create mode 100644 worklenz-backend/src/public/assets/outline/appstore.svg
create mode 100644 worklenz-backend/src/public/assets/outline/area-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/area-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-down.js
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-down.svg
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-up.js
create mode 100644 worklenz-backend/src/public/assets/outline/arrow-up.svg
create mode 100644 worklenz-backend/src/public/assets/outline/arrows-alt.js
create mode 100644 worklenz-backend/src/public/assets/outline/arrows-alt.svg
create mode 100644 worklenz-backend/src/public/assets/outline/audio-muted.js
create mode 100644 worklenz-backend/src/public/assets/outline/audio-muted.svg
create mode 100644 worklenz-backend/src/public/assets/outline/audio.js
create mode 100644 worklenz-backend/src/public/assets/outline/audio.svg
create mode 100644 worklenz-backend/src/public/assets/outline/audit.js
create mode 100644 worklenz-backend/src/public/assets/outline/audit.svg
create mode 100644 worklenz-backend/src/public/assets/outline/backward.js
create mode 100644 worklenz-backend/src/public/assets/outline/backward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bank.js
create mode 100644 worklenz-backend/src/public/assets/outline/bank.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bar-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/bar-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/barcode.js
create mode 100644 worklenz-backend/src/public/assets/outline/barcode.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bars.js
create mode 100644 worklenz-backend/src/public/assets/outline/bars.svg
create mode 100644 worklenz-backend/src/public/assets/outline/behance-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/behance-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/behance.js
create mode 100644 worklenz-backend/src/public/assets/outline/behance.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bell.js
create mode 100644 worklenz-backend/src/public/assets/outline/bell.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bg-colors.js
create mode 100644 worklenz-backend/src/public/assets/outline/bg-colors.svg
create mode 100644 worklenz-backend/src/public/assets/outline/block.js
create mode 100644 worklenz-backend/src/public/assets/outline/block.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bold.js
create mode 100644 worklenz-backend/src/public/assets/outline/bold.svg
create mode 100644 worklenz-backend/src/public/assets/outline/book.js
create mode 100644 worklenz-backend/src/public/assets/outline/book.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-bottom.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-bottom.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-horizontal.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-horizontal.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-inner.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-inner.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-outer.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-outer.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-top.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-top.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border-verticle.js
create mode 100644 worklenz-backend/src/public/assets/outline/border-verticle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/border.js
create mode 100644 worklenz-backend/src/public/assets/outline/border.svg
create mode 100644 worklenz-backend/src/public/assets/outline/borderless-table.js
create mode 100644 worklenz-backend/src/public/assets/outline/borderless-table.svg
create mode 100644 worklenz-backend/src/public/assets/outline/box-plot.js
create mode 100644 worklenz-backend/src/public/assets/outline/box-plot.svg
create mode 100644 worklenz-backend/src/public/assets/outline/branches.js
create mode 100644 worklenz-backend/src/public/assets/outline/branches.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bug.js
create mode 100644 worklenz-backend/src/public/assets/outline/bug.svg
create mode 100644 worklenz-backend/src/public/assets/outline/build.js
create mode 100644 worklenz-backend/src/public/assets/outline/build.svg
create mode 100644 worklenz-backend/src/public/assets/outline/bulb.js
create mode 100644 worklenz-backend/src/public/assets/outline/bulb.svg
create mode 100644 worklenz-backend/src/public/assets/outline/calculator.js
create mode 100644 worklenz-backend/src/public/assets/outline/calculator.svg
create mode 100644 worklenz-backend/src/public/assets/outline/calendar.js
create mode 100644 worklenz-backend/src/public/assets/outline/calendar.svg
create mode 100644 worklenz-backend/src/public/assets/outline/camera.js
create mode 100644 worklenz-backend/src/public/assets/outline/camera.svg
create mode 100644 worklenz-backend/src/public/assets/outline/car.js
create mode 100644 worklenz-backend/src/public/assets/outline/car.svg
create mode 100644 worklenz-backend/src/public/assets/outline/caret-down.js
create mode 100644 worklenz-backend/src/public/assets/outline/caret-down.svg
create mode 100644 worklenz-backend/src/public/assets/outline/caret-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/caret-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/caret-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/caret-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/caret-up.js
create mode 100644 worklenz-backend/src/public/assets/outline/caret-up.svg
create mode 100644 worklenz-backend/src/public/assets/outline/carry-out.js
create mode 100644 worklenz-backend/src/public/assets/outline/carry-out.svg
create mode 100644 worklenz-backend/src/public/assets/outline/check-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/check-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/check-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/check-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/check.js
create mode 100644 worklenz-backend/src/public/assets/outline/check.svg
create mode 100644 worklenz-backend/src/public/assets/outline/chrome.js
create mode 100644 worklenz-backend/src/public/assets/outline/chrome.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ci-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/ci-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ci.js
create mode 100644 worklenz-backend/src/public/assets/outline/ci.svg
create mode 100644 worklenz-backend/src/public/assets/outline/clear.js
create mode 100644 worklenz-backend/src/public/assets/outline/clear.svg
create mode 100644 worklenz-backend/src/public/assets/outline/clock-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/clock-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/close-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/close-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/close-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/close-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/close.js
create mode 100644 worklenz-backend/src/public/assets/outline/close.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-download.js
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-download.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-server.js
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-server.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-sync.js
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-sync.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-upload.js
create mode 100644 worklenz-backend/src/public/assets/outline/cloud-upload.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cloud.js
create mode 100644 worklenz-backend/src/public/assets/outline/cloud.svg
create mode 100644 worklenz-backend/src/public/assets/outline/cluster.js
create mode 100644 worklenz-backend/src/public/assets/outline/cluster.svg
create mode 100644 worklenz-backend/src/public/assets/outline/code-sandbox.js
create mode 100644 worklenz-backend/src/public/assets/outline/code-sandbox.svg
create mode 100644 worklenz-backend/src/public/assets/outline/code.js
create mode 100644 worklenz-backend/src/public/assets/outline/code.svg
create mode 100644 worklenz-backend/src/public/assets/outline/codepen-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/codepen-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/codepen.js
create mode 100644 worklenz-backend/src/public/assets/outline/codepen.svg
create mode 100644 worklenz-backend/src/public/assets/outline/coffee.js
create mode 100644 worklenz-backend/src/public/assets/outline/coffee.svg
create mode 100644 worklenz-backend/src/public/assets/outline/column-height.js
create mode 100644 worklenz-backend/src/public/assets/outline/column-height.svg
create mode 100644 worklenz-backend/src/public/assets/outline/column-width.js
create mode 100644 worklenz-backend/src/public/assets/outline/column-width.svg
create mode 100644 worklenz-backend/src/public/assets/outline/comment.js
create mode 100644 worklenz-backend/src/public/assets/outline/comment.svg
create mode 100644 worklenz-backend/src/public/assets/outline/compass.js
create mode 100644 worklenz-backend/src/public/assets/outline/compass.svg
create mode 100644 worklenz-backend/src/public/assets/outline/compress.js
create mode 100644 worklenz-backend/src/public/assets/outline/compress.svg
create mode 100644 worklenz-backend/src/public/assets/outline/console-sql.js
create mode 100644 worklenz-backend/src/public/assets/outline/console-sql.svg
create mode 100644 worklenz-backend/src/public/assets/outline/contacts.js
create mode 100644 worklenz-backend/src/public/assets/outline/contacts.svg
create mode 100644 worklenz-backend/src/public/assets/outline/container.js
create mode 100644 worklenz-backend/src/public/assets/outline/container.svg
create mode 100644 worklenz-backend/src/public/assets/outline/control.js
create mode 100644 worklenz-backend/src/public/assets/outline/control.svg
create mode 100644 worklenz-backend/src/public/assets/outline/copy.js
create mode 100644 worklenz-backend/src/public/assets/outline/copy.svg
create mode 100644 worklenz-backend/src/public/assets/outline/copyright-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/copyright-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/copyright.js
create mode 100644 worklenz-backend/src/public/assets/outline/copyright.svg
create mode 100644 worklenz-backend/src/public/assets/outline/credit-card.js
create mode 100644 worklenz-backend/src/public/assets/outline/credit-card.svg
create mode 100644 worklenz-backend/src/public/assets/outline/crown.js
create mode 100644 worklenz-backend/src/public/assets/outline/crown.svg
create mode 100644 worklenz-backend/src/public/assets/outline/customer-service.js
create mode 100644 worklenz-backend/src/public/assets/outline/customer-service.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dash.js
create mode 100644 worklenz-backend/src/public/assets/outline/dash.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dashboard.js
create mode 100644 worklenz-backend/src/public/assets/outline/dashboard.svg
create mode 100644 worklenz-backend/src/public/assets/outline/database.js
create mode 100644 worklenz-backend/src/public/assets/outline/database.svg
create mode 100644 worklenz-backend/src/public/assets/outline/delete-column.js
create mode 100644 worklenz-backend/src/public/assets/outline/delete-column.svg
create mode 100644 worklenz-backend/src/public/assets/outline/delete-row.js
create mode 100644 worklenz-backend/src/public/assets/outline/delete-row.svg
create mode 100644 worklenz-backend/src/public/assets/outline/delete.js
create mode 100644 worklenz-backend/src/public/assets/outline/delete.svg
create mode 100644 worklenz-backend/src/public/assets/outline/delivered-procedure.js
create mode 100644 worklenz-backend/src/public/assets/outline/delivered-procedure.svg
create mode 100644 worklenz-backend/src/public/assets/outline/deployment-unit.js
create mode 100644 worklenz-backend/src/public/assets/outline/deployment-unit.svg
create mode 100644 worklenz-backend/src/public/assets/outline/desktop.js
create mode 100644 worklenz-backend/src/public/assets/outline/desktop.svg
create mode 100644 worklenz-backend/src/public/assets/outline/diff.js
create mode 100644 worklenz-backend/src/public/assets/outline/diff.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dingding.js
create mode 100644 worklenz-backend/src/public/assets/outline/dingding.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dingtalk.js
create mode 100644 worklenz-backend/src/public/assets/outline/dingtalk.svg
create mode 100644 worklenz-backend/src/public/assets/outline/disconnect.js
create mode 100644 worklenz-backend/src/public/assets/outline/disconnect.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dislike.js
create mode 100644 worklenz-backend/src/public/assets/outline/dislike.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dollar-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/dollar-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dollar.js
create mode 100644 worklenz-backend/src/public/assets/outline/dollar.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dot-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/dot-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/double-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/double-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/double-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/double-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/down-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/down-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/down-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/down-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/down.js
create mode 100644 worklenz-backend/src/public/assets/outline/down.svg
create mode 100644 worklenz-backend/src/public/assets/outline/download.js
create mode 100644 worklenz-backend/src/public/assets/outline/download.svg
create mode 100644 worklenz-backend/src/public/assets/outline/drag.js
create mode 100644 worklenz-backend/src/public/assets/outline/drag.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dribbble-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/dribbble-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dribbble.js
create mode 100644 worklenz-backend/src/public/assets/outline/dribbble.svg
create mode 100644 worklenz-backend/src/public/assets/outline/dropbox.js
create mode 100644 worklenz-backend/src/public/assets/outline/dropbox.svg
create mode 100644 worklenz-backend/src/public/assets/outline/edit.js
create mode 100644 worklenz-backend/src/public/assets/outline/edit.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ellipsis.js
create mode 100644 worklenz-backend/src/public/assets/outline/ellipsis.svg
create mode 100644 worklenz-backend/src/public/assets/outline/enter.js
create mode 100644 worklenz-backend/src/public/assets/outline/enter.svg
create mode 100644 worklenz-backend/src/public/assets/outline/environment.js
create mode 100644 worklenz-backend/src/public/assets/outline/environment.svg
create mode 100644 worklenz-backend/src/public/assets/outline/euro-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/euro-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/euro.js
create mode 100644 worklenz-backend/src/public/assets/outline/euro.svg
create mode 100644 worklenz-backend/src/public/assets/outline/exception.js
create mode 100644 worklenz-backend/src/public/assets/outline/exception.svg
create mode 100644 worklenz-backend/src/public/assets/outline/exclamation-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/exclamation-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/exclamation.js
create mode 100644 worklenz-backend/src/public/assets/outline/exclamation.svg
create mode 100644 worklenz-backend/src/public/assets/outline/expand-alt.js
create mode 100644 worklenz-backend/src/public/assets/outline/expand-alt.svg
create mode 100644 worklenz-backend/src/public/assets/outline/expand.js
create mode 100644 worklenz-backend/src/public/assets/outline/expand.svg
create mode 100644 worklenz-backend/src/public/assets/outline/experiment.js
create mode 100644 worklenz-backend/src/public/assets/outline/experiment.svg
create mode 100644 worklenz-backend/src/public/assets/outline/export.js
create mode 100644 worklenz-backend/src/public/assets/outline/export.svg
create mode 100644 worklenz-backend/src/public/assets/outline/eye-invisible.js
create mode 100644 worklenz-backend/src/public/assets/outline/eye-invisible.svg
create mode 100644 worklenz-backend/src/public/assets/outline/eye.js
create mode 100644 worklenz-backend/src/public/assets/outline/eye.svg
create mode 100644 worklenz-backend/src/public/assets/outline/facebook.js
create mode 100644 worklenz-backend/src/public/assets/outline/facebook.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fall.js
create mode 100644 worklenz-backend/src/public/assets/outline/fall.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fast-backward.js
create mode 100644 worklenz-backend/src/public/assets/outline/fast-backward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fast-forward.js
create mode 100644 worklenz-backend/src/public/assets/outline/fast-forward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/field-binary.js
create mode 100644 worklenz-backend/src/public/assets/outline/field-binary.svg
create mode 100644 worklenz-backend/src/public/assets/outline/field-number.js
create mode 100644 worklenz-backend/src/public/assets/outline/field-number.svg
create mode 100644 worklenz-backend/src/public/assets/outline/field-string.js
create mode 100644 worklenz-backend/src/public/assets/outline/field-string.svg
create mode 100644 worklenz-backend/src/public/assets/outline/field-time.js
create mode 100644 worklenz-backend/src/public/assets/outline/field-time.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-done.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-done.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-excel.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-excel.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-exclamation.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-exclamation.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-gif.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-gif.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-image.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-image.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-jpg.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-jpg.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-markdown.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-markdown.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-pdf.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-pdf.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-ppt.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-ppt.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-protect.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-protect.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-search.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-search.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-sync.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-sync.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-text.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-text.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-unknown.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-unknown.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-word.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-word.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file-zip.js
create mode 100644 worklenz-backend/src/public/assets/outline/file-zip.svg
create mode 100644 worklenz-backend/src/public/assets/outline/file.js
create mode 100644 worklenz-backend/src/public/assets/outline/file.svg
create mode 100644 worklenz-backend/src/public/assets/outline/filter.js
create mode 100644 worklenz-backend/src/public/assets/outline/filter.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fire.js
create mode 100644 worklenz-backend/src/public/assets/outline/fire.svg
create mode 100644 worklenz-backend/src/public/assets/outline/flag.js
create mode 100644 worklenz-backend/src/public/assets/outline/flag.svg
create mode 100644 worklenz-backend/src/public/assets/outline/folder-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/folder-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/folder-open.js
create mode 100644 worklenz-backend/src/public/assets/outline/folder-open.svg
create mode 100644 worklenz-backend/src/public/assets/outline/folder-view.js
create mode 100644 worklenz-backend/src/public/assets/outline/folder-view.svg
create mode 100644 worklenz-backend/src/public/assets/outline/folder.js
create mode 100644 worklenz-backend/src/public/assets/outline/folder.svg
create mode 100644 worklenz-backend/src/public/assets/outline/font-colors.js
create mode 100644 worklenz-backend/src/public/assets/outline/font-colors.svg
create mode 100644 worklenz-backend/src/public/assets/outline/font-size.js
create mode 100644 worklenz-backend/src/public/assets/outline/font-size.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fork.js
create mode 100644 worklenz-backend/src/public/assets/outline/fork.svg
create mode 100644 worklenz-backend/src/public/assets/outline/form.js
create mode 100644 worklenz-backend/src/public/assets/outline/form.svg
create mode 100644 worklenz-backend/src/public/assets/outline/format-painter.js
create mode 100644 worklenz-backend/src/public/assets/outline/format-painter.svg
create mode 100644 worklenz-backend/src/public/assets/outline/forward.js
create mode 100644 worklenz-backend/src/public/assets/outline/forward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/frown.js
create mode 100644 worklenz-backend/src/public/assets/outline/frown.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fullscreen-exit.js
create mode 100644 worklenz-backend/src/public/assets/outline/fullscreen-exit.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fullscreen.js
create mode 100644 worklenz-backend/src/public/assets/outline/fullscreen.svg
create mode 100644 worklenz-backend/src/public/assets/outline/function.js
create mode 100644 worklenz-backend/src/public/assets/outline/function.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fund-projection-screen.js
create mode 100644 worklenz-backend/src/public/assets/outline/fund-projection-screen.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fund-view.js
create mode 100644 worklenz-backend/src/public/assets/outline/fund-view.svg
create mode 100644 worklenz-backend/src/public/assets/outline/fund.js
create mode 100644 worklenz-backend/src/public/assets/outline/fund.svg
create mode 100644 worklenz-backend/src/public/assets/outline/funnel-plot.js
create mode 100644 worklenz-backend/src/public/assets/outline/funnel-plot.svg
create mode 100644 worklenz-backend/src/public/assets/outline/gateway.js
create mode 100644 worklenz-backend/src/public/assets/outline/gateway.svg
create mode 100644 worklenz-backend/src/public/assets/outline/gif.js
create mode 100644 worklenz-backend/src/public/assets/outline/gif.svg
create mode 100644 worklenz-backend/src/public/assets/outline/gift.js
create mode 100644 worklenz-backend/src/public/assets/outline/gift.svg
create mode 100644 worklenz-backend/src/public/assets/outline/github.js
create mode 100644 worklenz-backend/src/public/assets/outline/github.svg
create mode 100644 worklenz-backend/src/public/assets/outline/gitlab.js
create mode 100644 worklenz-backend/src/public/assets/outline/gitlab.svg
create mode 100644 worklenz-backend/src/public/assets/outline/global.js
create mode 100644 worklenz-backend/src/public/assets/outline/global.svg
create mode 100644 worklenz-backend/src/public/assets/outline/gold.js
create mode 100644 worklenz-backend/src/public/assets/outline/gold.svg
create mode 100644 worklenz-backend/src/public/assets/outline/google-plus.js
create mode 100644 worklenz-backend/src/public/assets/outline/google-plus.svg
create mode 100644 worklenz-backend/src/public/assets/outline/google.js
create mode 100644 worklenz-backend/src/public/assets/outline/google.svg
create mode 100644 worklenz-backend/src/public/assets/outline/group.js
create mode 100644 worklenz-backend/src/public/assets/outline/group.svg
create mode 100644 worklenz-backend/src/public/assets/outline/hdd.js
create mode 100644 worklenz-backend/src/public/assets/outline/hdd.svg
create mode 100644 worklenz-backend/src/public/assets/outline/heart.js
create mode 100644 worklenz-backend/src/public/assets/outline/heart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/heat-map.js
create mode 100644 worklenz-backend/src/public/assets/outline/heat-map.svg
create mode 100644 worklenz-backend/src/public/assets/outline/highlight.js
create mode 100644 worklenz-backend/src/public/assets/outline/highlight.svg
create mode 100644 worklenz-backend/src/public/assets/outline/history.js
create mode 100644 worklenz-backend/src/public/assets/outline/history.svg
create mode 100644 worklenz-backend/src/public/assets/outline/holder.js
create mode 100644 worklenz-backend/src/public/assets/outline/holder.svg
create mode 100644 worklenz-backend/src/public/assets/outline/home.js
create mode 100644 worklenz-backend/src/public/assets/outline/home.svg
create mode 100644 worklenz-backend/src/public/assets/outline/hourglass.js
create mode 100644 worklenz-backend/src/public/assets/outline/hourglass.svg
create mode 100644 worklenz-backend/src/public/assets/outline/html5.js
create mode 100644 worklenz-backend/src/public/assets/outline/html5.svg
create mode 100644 worklenz-backend/src/public/assets/outline/idcard.js
create mode 100644 worklenz-backend/src/public/assets/outline/idcard.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ie.js
create mode 100644 worklenz-backend/src/public/assets/outline/ie.svg
create mode 100644 worklenz-backend/src/public/assets/outline/import.js
create mode 100644 worklenz-backend/src/public/assets/outline/import.svg
create mode 100644 worklenz-backend/src/public/assets/outline/inbox.js
create mode 100644 worklenz-backend/src/public/assets/outline/inbox.svg
create mode 100644 worklenz-backend/src/public/assets/outline/info-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/info-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/info.js
create mode 100644 worklenz-backend/src/public/assets/outline/info.svg
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-above.js
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-above.svg
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-below.js
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-below.svg
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/insert-row-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/instagram.js
create mode 100644 worklenz-backend/src/public/assets/outline/instagram.svg
create mode 100644 worklenz-backend/src/public/assets/outline/insurance.js
create mode 100644 worklenz-backend/src/public/assets/outline/insurance.svg
create mode 100644 worklenz-backend/src/public/assets/outline/interaction.js
create mode 100644 worklenz-backend/src/public/assets/outline/interaction.svg
create mode 100644 worklenz-backend/src/public/assets/outline/issues-close.js
create mode 100644 worklenz-backend/src/public/assets/outline/issues-close.svg
create mode 100644 worklenz-backend/src/public/assets/outline/italic.js
create mode 100644 worklenz-backend/src/public/assets/outline/italic.svg
create mode 100644 worklenz-backend/src/public/assets/outline/key.js
create mode 100644 worklenz-backend/src/public/assets/outline/key.svg
create mode 100644 worklenz-backend/src/public/assets/outline/laptop.js
create mode 100644 worklenz-backend/src/public/assets/outline/laptop.svg
create mode 100644 worklenz-backend/src/public/assets/outline/layout.js
create mode 100644 worklenz-backend/src/public/assets/outline/layout.svg
create mode 100644 worklenz-backend/src/public/assets/outline/left-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/left-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/left-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/left-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/left.js
create mode 100644 worklenz-backend/src/public/assets/outline/left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/like.js
create mode 100644 worklenz-backend/src/public/assets/outline/like.svg
create mode 100644 worklenz-backend/src/public/assets/outline/line-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/line-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/line-height.js
create mode 100644 worklenz-backend/src/public/assets/outline/line-height.svg
create mode 100644 worklenz-backend/src/public/assets/outline/line.js
create mode 100644 worklenz-backend/src/public/assets/outline/line.svg
create mode 100644 worklenz-backend/src/public/assets/outline/link.js
create mode 100644 worklenz-backend/src/public/assets/outline/link.svg
create mode 100644 worklenz-backend/src/public/assets/outline/linkedin.js
create mode 100644 worklenz-backend/src/public/assets/outline/linkedin.svg
create mode 100644 worklenz-backend/src/public/assets/outline/loading-3-quarters.js
create mode 100644 worklenz-backend/src/public/assets/outline/loading-3-quarters.svg
create mode 100644 worklenz-backend/src/public/assets/outline/loading.js
create mode 100644 worklenz-backend/src/public/assets/outline/loading.svg
create mode 100644 worklenz-backend/src/public/assets/outline/lock.js
create mode 100644 worklenz-backend/src/public/assets/outline/lock.svg
create mode 100644 worklenz-backend/src/public/assets/outline/login.js
create mode 100644 worklenz-backend/src/public/assets/outline/login.svg
create mode 100644 worklenz-backend/src/public/assets/outline/logout.js
create mode 100644 worklenz-backend/src/public/assets/outline/logout.svg
create mode 100644 worklenz-backend/src/public/assets/outline/mac-command.js
create mode 100644 worklenz-backend/src/public/assets/outline/mac-command.svg
create mode 100644 worklenz-backend/src/public/assets/outline/mail.js
create mode 100644 worklenz-backend/src/public/assets/outline/mail.svg
create mode 100644 worklenz-backend/src/public/assets/outline/man.js
create mode 100644 worklenz-backend/src/public/assets/outline/man.svg
create mode 100644 worklenz-backend/src/public/assets/outline/medicine-box.js
create mode 100644 worklenz-backend/src/public/assets/outline/medicine-box.svg
create mode 100644 worklenz-backend/src/public/assets/outline/medium-workmark.js
create mode 100644 worklenz-backend/src/public/assets/outline/medium-workmark.svg
create mode 100644 worklenz-backend/src/public/assets/outline/medium.js
create mode 100644 worklenz-backend/src/public/assets/outline/medium.svg
create mode 100644 worklenz-backend/src/public/assets/outline/meh.js
create mode 100644 worklenz-backend/src/public/assets/outline/meh.svg
create mode 100644 worklenz-backend/src/public/assets/outline/menu-fold.js
create mode 100644 worklenz-backend/src/public/assets/outline/menu-fold.svg
create mode 100644 worklenz-backend/src/public/assets/outline/menu-unfold.js
create mode 100644 worklenz-backend/src/public/assets/outline/menu-unfold.svg
create mode 100644 worklenz-backend/src/public/assets/outline/menu.js
create mode 100644 worklenz-backend/src/public/assets/outline/menu.svg
create mode 100644 worklenz-backend/src/public/assets/outline/merge-cells.js
create mode 100644 worklenz-backend/src/public/assets/outline/merge-cells.svg
create mode 100644 worklenz-backend/src/public/assets/outline/message.js
create mode 100644 worklenz-backend/src/public/assets/outline/message.svg
create mode 100644 worklenz-backend/src/public/assets/outline/minus-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/minus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/minus-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/minus-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/minus.js
create mode 100644 worklenz-backend/src/public/assets/outline/minus.svg
create mode 100644 worklenz-backend/src/public/assets/outline/mobile.js
create mode 100644 worklenz-backend/src/public/assets/outline/mobile.svg
create mode 100644 worklenz-backend/src/public/assets/outline/money-collect.js
create mode 100644 worklenz-backend/src/public/assets/outline/money-collect.svg
create mode 100644 worklenz-backend/src/public/assets/outline/monitor.js
create mode 100644 worklenz-backend/src/public/assets/outline/monitor.svg
create mode 100644 worklenz-backend/src/public/assets/outline/more.js
create mode 100644 worklenz-backend/src/public/assets/outline/more.svg
create mode 100644 worklenz-backend/src/public/assets/outline/node-collapse.js
create mode 100644 worklenz-backend/src/public/assets/outline/node-collapse.svg
create mode 100644 worklenz-backend/src/public/assets/outline/node-expand.js
create mode 100644 worklenz-backend/src/public/assets/outline/node-expand.svg
create mode 100644 worklenz-backend/src/public/assets/outline/node-index.js
create mode 100644 worklenz-backend/src/public/assets/outline/node-index.svg
create mode 100644 worklenz-backend/src/public/assets/outline/notification.js
create mode 100644 worklenz-backend/src/public/assets/outline/notification.svg
create mode 100644 worklenz-backend/src/public/assets/outline/number.js
create mode 100644 worklenz-backend/src/public/assets/outline/number.svg
create mode 100644 worklenz-backend/src/public/assets/outline/one-to-one.js
create mode 100644 worklenz-backend/src/public/assets/outline/one-to-one.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ordered-list.js
create mode 100644 worklenz-backend/src/public/assets/outline/ordered-list.svg
create mode 100644 worklenz-backend/src/public/assets/outline/paper-clip.js
create mode 100644 worklenz-backend/src/public/assets/outline/paper-clip.svg
create mode 100644 worklenz-backend/src/public/assets/outline/partition.js
create mode 100644 worklenz-backend/src/public/assets/outline/partition.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pause-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/pause-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pause.js
create mode 100644 worklenz-backend/src/public/assets/outline/pause.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pay-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/pay-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/percentage.js
create mode 100644 worklenz-backend/src/public/assets/outline/percentage.svg
create mode 100644 worklenz-backend/src/public/assets/outline/phone.js
create mode 100644 worklenz-backend/src/public/assets/outline/phone.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pic-center.js
create mode 100644 worklenz-backend/src/public/assets/outline/pic-center.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pic-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/pic-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pic-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/pic-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/picture.js
create mode 100644 worklenz-backend/src/public/assets/outline/picture.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pie-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/pie-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/play-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/play-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/play-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/play-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/plus-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/plus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/plus-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/plus-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/plus.js
create mode 100644 worklenz-backend/src/public/assets/outline/plus.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pound-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/pound-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pound.js
create mode 100644 worklenz-backend/src/public/assets/outline/pound.svg
create mode 100644 worklenz-backend/src/public/assets/outline/poweroff.js
create mode 100644 worklenz-backend/src/public/assets/outline/poweroff.svg
create mode 100644 worklenz-backend/src/public/assets/outline/printer.js
create mode 100644 worklenz-backend/src/public/assets/outline/printer.svg
create mode 100644 worklenz-backend/src/public/assets/outline/profile.js
create mode 100644 worklenz-backend/src/public/assets/outline/profile.svg
create mode 100644 worklenz-backend/src/public/assets/outline/project.js
create mode 100644 worklenz-backend/src/public/assets/outline/project.svg
create mode 100644 worklenz-backend/src/public/assets/outline/property-safety.js
create mode 100644 worklenz-backend/src/public/assets/outline/property-safety.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pull-request.js
create mode 100644 worklenz-backend/src/public/assets/outline/pull-request.svg
create mode 100644 worklenz-backend/src/public/assets/outline/pushpin.js
create mode 100644 worklenz-backend/src/public/assets/outline/pushpin.svg
create mode 100644 worklenz-backend/src/public/assets/outline/qq.js
create mode 100644 worklenz-backend/src/public/assets/outline/qq.svg
create mode 100644 worklenz-backend/src/public/assets/outline/qrcode.js
create mode 100644 worklenz-backend/src/public/assets/outline/qrcode.svg
create mode 100644 worklenz-backend/src/public/assets/outline/question-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/question-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/question.js
create mode 100644 worklenz-backend/src/public/assets/outline/question.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radar-chart.js
create mode 100644 worklenz-backend/src/public/assets/outline/radar-chart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radius-bottomleft.js
create mode 100644 worklenz-backend/src/public/assets/outline/radius-bottomleft.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radius-bottomright.js
create mode 100644 worklenz-backend/src/public/assets/outline/radius-bottomright.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radius-setting.js
create mode 100644 worklenz-backend/src/public/assets/outline/radius-setting.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radius-upleft.js
create mode 100644 worklenz-backend/src/public/assets/outline/radius-upleft.svg
create mode 100644 worklenz-backend/src/public/assets/outline/radius-upright.js
create mode 100644 worklenz-backend/src/public/assets/outline/radius-upright.svg
create mode 100644 worklenz-backend/src/public/assets/outline/read.js
create mode 100644 worklenz-backend/src/public/assets/outline/read.svg
create mode 100644 worklenz-backend/src/public/assets/outline/reconciliation.js
create mode 100644 worklenz-backend/src/public/assets/outline/reconciliation.svg
create mode 100644 worklenz-backend/src/public/assets/outline/red-envelope.js
create mode 100644 worklenz-backend/src/public/assets/outline/red-envelope.svg
create mode 100644 worklenz-backend/src/public/assets/outline/reddit.js
create mode 100644 worklenz-backend/src/public/assets/outline/reddit.svg
create mode 100644 worklenz-backend/src/public/assets/outline/redo.js
create mode 100644 worklenz-backend/src/public/assets/outline/redo.svg
create mode 100644 worklenz-backend/src/public/assets/outline/reload.js
create mode 100644 worklenz-backend/src/public/assets/outline/reload.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rest.js
create mode 100644 worklenz-backend/src/public/assets/outline/rest.svg
create mode 100644 worklenz-backend/src/public/assets/outline/retweet.js
create mode 100644 worklenz-backend/src/public/assets/outline/retweet.svg
create mode 100644 worklenz-backend/src/public/assets/outline/right-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/right-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/right-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/right-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/right.js
create mode 100644 worklenz-backend/src/public/assets/outline/right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rise.js
create mode 100644 worklenz-backend/src/public/assets/outline/rise.svg
create mode 100644 worklenz-backend/src/public/assets/outline/robot.js
create mode 100644 worklenz-backend/src/public/assets/outline/robot.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rocket.js
create mode 100644 worklenz-backend/src/public/assets/outline/rocket.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rollback.js
create mode 100644 worklenz-backend/src/public/assets/outline/rollback.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rotate-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/rotate-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/rotate-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/rotate-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/safety-certificate.js
create mode 100644 worklenz-backend/src/public/assets/outline/safety-certificate.svg
create mode 100644 worklenz-backend/src/public/assets/outline/safety.js
create mode 100644 worklenz-backend/src/public/assets/outline/safety.svg
create mode 100644 worklenz-backend/src/public/assets/outline/save.js
create mode 100644 worklenz-backend/src/public/assets/outline/save.svg
create mode 100644 worklenz-backend/src/public/assets/outline/scan.js
create mode 100644 worklenz-backend/src/public/assets/outline/scan.svg
create mode 100644 worklenz-backend/src/public/assets/outline/schedule.js
create mode 100644 worklenz-backend/src/public/assets/outline/schedule.svg
create mode 100644 worklenz-backend/src/public/assets/outline/scissor.js
create mode 100644 worklenz-backend/src/public/assets/outline/scissor.svg
create mode 100644 worklenz-backend/src/public/assets/outline/search.js
create mode 100644 worklenz-backend/src/public/assets/outline/search.svg
create mode 100644 worklenz-backend/src/public/assets/outline/security-scan.js
create mode 100644 worklenz-backend/src/public/assets/outline/security-scan.svg
create mode 100644 worklenz-backend/src/public/assets/outline/select.js
create mode 100644 worklenz-backend/src/public/assets/outline/select.svg
create mode 100644 worklenz-backend/src/public/assets/outline/send.js
create mode 100644 worklenz-backend/src/public/assets/outline/send.svg
create mode 100644 worklenz-backend/src/public/assets/outline/setting.js
create mode 100644 worklenz-backend/src/public/assets/outline/setting.svg
create mode 100644 worklenz-backend/src/public/assets/outline/shake.js
create mode 100644 worklenz-backend/src/public/assets/outline/shake.svg
create mode 100644 worklenz-backend/src/public/assets/outline/share-alt.js
create mode 100644 worklenz-backend/src/public/assets/outline/share-alt.svg
create mode 100644 worklenz-backend/src/public/assets/outline/shop.js
create mode 100644 worklenz-backend/src/public/assets/outline/shop.svg
create mode 100644 worklenz-backend/src/public/assets/outline/shopping-cart.js
create mode 100644 worklenz-backend/src/public/assets/outline/shopping-cart.svg
create mode 100644 worklenz-backend/src/public/assets/outline/shopping.js
create mode 100644 worklenz-backend/src/public/assets/outline/shopping.svg
create mode 100644 worklenz-backend/src/public/assets/outline/shrink.js
create mode 100644 worklenz-backend/src/public/assets/outline/shrink.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sisternode.js
create mode 100644 worklenz-backend/src/public/assets/outline/sisternode.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sketch.js
create mode 100644 worklenz-backend/src/public/assets/outline/sketch.svg
create mode 100644 worklenz-backend/src/public/assets/outline/skin.js
create mode 100644 worklenz-backend/src/public/assets/outline/skin.svg
create mode 100644 worklenz-backend/src/public/assets/outline/skype.js
create mode 100644 worklenz-backend/src/public/assets/outline/skype.svg
create mode 100644 worklenz-backend/src/public/assets/outline/slack-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/slack-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/slack.js
create mode 100644 worklenz-backend/src/public/assets/outline/slack.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sliders.js
create mode 100644 worklenz-backend/src/public/assets/outline/sliders.svg
create mode 100644 worklenz-backend/src/public/assets/outline/small-dash.js
create mode 100644 worklenz-backend/src/public/assets/outline/small-dash.svg
create mode 100644 worklenz-backend/src/public/assets/outline/smile.js
create mode 100644 worklenz-backend/src/public/assets/outline/smile.svg
create mode 100644 worklenz-backend/src/public/assets/outline/snippets.js
create mode 100644 worklenz-backend/src/public/assets/outline/snippets.svg
create mode 100644 worklenz-backend/src/public/assets/outline/solution.js
create mode 100644 worklenz-backend/src/public/assets/outline/solution.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sort-ascending.js
create mode 100644 worklenz-backend/src/public/assets/outline/sort-ascending.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sort-descending.js
create mode 100644 worklenz-backend/src/public/assets/outline/sort-descending.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sound.js
create mode 100644 worklenz-backend/src/public/assets/outline/sound.svg
create mode 100644 worklenz-backend/src/public/assets/outline/split-cells.js
create mode 100644 worklenz-backend/src/public/assets/outline/split-cells.svg
create mode 100644 worklenz-backend/src/public/assets/outline/star.js
create mode 100644 worklenz-backend/src/public/assets/outline/star.svg
create mode 100644 worklenz-backend/src/public/assets/outline/step-backward.js
create mode 100644 worklenz-backend/src/public/assets/outline/step-backward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/step-forward.js
create mode 100644 worklenz-backend/src/public/assets/outline/step-forward.svg
create mode 100644 worklenz-backend/src/public/assets/outline/stock.js
create mode 100644 worklenz-backend/src/public/assets/outline/stock.svg
create mode 100644 worklenz-backend/src/public/assets/outline/stop.js
create mode 100644 worklenz-backend/src/public/assets/outline/stop.svg
create mode 100644 worklenz-backend/src/public/assets/outline/strikethrough.js
create mode 100644 worklenz-backend/src/public/assets/outline/strikethrough.svg
create mode 100644 worklenz-backend/src/public/assets/outline/subnode.js
create mode 100644 worklenz-backend/src/public/assets/outline/subnode.svg
create mode 100644 worklenz-backend/src/public/assets/outline/swap-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/swap-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/swap-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/swap-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/swap.js
create mode 100644 worklenz-backend/src/public/assets/outline/swap.svg
create mode 100644 worklenz-backend/src/public/assets/outline/switcher.js
create mode 100644 worklenz-backend/src/public/assets/outline/switcher.svg
create mode 100644 worklenz-backend/src/public/assets/outline/sync.js
create mode 100644 worklenz-backend/src/public/assets/outline/sync.svg
create mode 100644 worklenz-backend/src/public/assets/outline/table.js
create mode 100644 worklenz-backend/src/public/assets/outline/table.svg
create mode 100644 worklenz-backend/src/public/assets/outline/tablet.js
create mode 100644 worklenz-backend/src/public/assets/outline/tablet.svg
create mode 100644 worklenz-backend/src/public/assets/outline/tag.js
create mode 100644 worklenz-backend/src/public/assets/outline/tag.svg
create mode 100644 worklenz-backend/src/public/assets/outline/tags.js
create mode 100644 worklenz-backend/src/public/assets/outline/tags.svg
create mode 100644 worklenz-backend/src/public/assets/outline/taobao-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/taobao-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/taobao.js
create mode 100644 worklenz-backend/src/public/assets/outline/taobao.svg
create mode 100644 worklenz-backend/src/public/assets/outline/team.js
create mode 100644 worklenz-backend/src/public/assets/outline/team.svg
create mode 100644 worklenz-backend/src/public/assets/outline/thunderbolt.js
create mode 100644 worklenz-backend/src/public/assets/outline/thunderbolt.svg
create mode 100644 worklenz-backend/src/public/assets/outline/to-top.js
create mode 100644 worklenz-backend/src/public/assets/outline/to-top.svg
create mode 100644 worklenz-backend/src/public/assets/outline/tool.js
create mode 100644 worklenz-backend/src/public/assets/outline/tool.svg
create mode 100644 worklenz-backend/src/public/assets/outline/trademark-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/trademark-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/trademark.js
create mode 100644 worklenz-backend/src/public/assets/outline/trademark.svg
create mode 100644 worklenz-backend/src/public/assets/outline/transaction.js
create mode 100644 worklenz-backend/src/public/assets/outline/transaction.svg
create mode 100644 worklenz-backend/src/public/assets/outline/translation.js
create mode 100644 worklenz-backend/src/public/assets/outline/translation.svg
create mode 100644 worklenz-backend/src/public/assets/outline/trophy.js
create mode 100644 worklenz-backend/src/public/assets/outline/trophy.svg
create mode 100644 worklenz-backend/src/public/assets/outline/twitter.js
create mode 100644 worklenz-backend/src/public/assets/outline/twitter.svg
create mode 100644 worklenz-backend/src/public/assets/outline/underline.js
create mode 100644 worklenz-backend/src/public/assets/outline/underline.svg
create mode 100644 worklenz-backend/src/public/assets/outline/undo.js
create mode 100644 worklenz-backend/src/public/assets/outline/undo.svg
create mode 100644 worklenz-backend/src/public/assets/outline/ungroup.js
create mode 100644 worklenz-backend/src/public/assets/outline/ungroup.svg
create mode 100644 worklenz-backend/src/public/assets/outline/unlock.js
create mode 100644 worklenz-backend/src/public/assets/outline/unlock.svg
create mode 100644 worklenz-backend/src/public/assets/outline/unordered-list.js
create mode 100644 worklenz-backend/src/public/assets/outline/unordered-list.svg
create mode 100644 worklenz-backend/src/public/assets/outline/up-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/up-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/up-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/up-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/up.js
create mode 100644 worklenz-backend/src/public/assets/outline/up.svg
create mode 100644 worklenz-backend/src/public/assets/outline/upload.js
create mode 100644 worklenz-backend/src/public/assets/outline/upload.svg
create mode 100644 worklenz-backend/src/public/assets/outline/usb.js
create mode 100644 worklenz-backend/src/public/assets/outline/usb.svg
create mode 100644 worklenz-backend/src/public/assets/outline/user-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/user-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/user-delete.js
create mode 100644 worklenz-backend/src/public/assets/outline/user-delete.svg
create mode 100644 worklenz-backend/src/public/assets/outline/user-switch.js
create mode 100644 worklenz-backend/src/public/assets/outline/user-switch.svg
create mode 100644 worklenz-backend/src/public/assets/outline/user.js
create mode 100644 worklenz-backend/src/public/assets/outline/user.svg
create mode 100644 worklenz-backend/src/public/assets/outline/usergroup-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/usergroup-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/usergroup-delete.js
create mode 100644 worklenz-backend/src/public/assets/outline/usergroup-delete.svg
create mode 100644 worklenz-backend/src/public/assets/outline/verified.js
create mode 100644 worklenz-backend/src/public/assets/outline/verified.svg
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-bottom.js
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-bottom.svg
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-middle.js
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-middle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-top.js
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-align-top.svg
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-left.js
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-left.svg
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-right.js
create mode 100644 worklenz-backend/src/public/assets/outline/vertical-right.svg
create mode 100644 worklenz-backend/src/public/assets/outline/video-camera-add.js
create mode 100644 worklenz-backend/src/public/assets/outline/video-camera-add.svg
create mode 100644 worklenz-backend/src/public/assets/outline/video-camera.js
create mode 100644 worklenz-backend/src/public/assets/outline/video-camera.svg
create mode 100644 worklenz-backend/src/public/assets/outline/wallet.js
create mode 100644 worklenz-backend/src/public/assets/outline/wallet.svg
create mode 100644 worklenz-backend/src/public/assets/outline/warning.js
create mode 100644 worklenz-backend/src/public/assets/outline/warning.svg
create mode 100644 worklenz-backend/src/public/assets/outline/wechat.js
create mode 100644 worklenz-backend/src/public/assets/outline/wechat.svg
create mode 100644 worklenz-backend/src/public/assets/outline/weibo-circle.js
create mode 100644 worklenz-backend/src/public/assets/outline/weibo-circle.svg
create mode 100644 worklenz-backend/src/public/assets/outline/weibo-square.js
create mode 100644 worklenz-backend/src/public/assets/outline/weibo-square.svg
create mode 100644 worklenz-backend/src/public/assets/outline/weibo.js
create mode 100644 worklenz-backend/src/public/assets/outline/weibo.svg
create mode 100644 worklenz-backend/src/public/assets/outline/whats-app.js
create mode 100644 worklenz-backend/src/public/assets/outline/whats-app.svg
create mode 100644 worklenz-backend/src/public/assets/outline/wifi.js
create mode 100644 worklenz-backend/src/public/assets/outline/wifi.svg
create mode 100644 worklenz-backend/src/public/assets/outline/windows.js
create mode 100644 worklenz-backend/src/public/assets/outline/windows.svg
create mode 100644 worklenz-backend/src/public/assets/outline/woman.js
create mode 100644 worklenz-backend/src/public/assets/outline/woman.svg
create mode 100644 worklenz-backend/src/public/assets/outline/yahoo.js
create mode 100644 worklenz-backend/src/public/assets/outline/yahoo.svg
create mode 100644 worklenz-backend/src/public/assets/outline/youtube.js
create mode 100644 worklenz-backend/src/public/assets/outline/youtube.svg
create mode 100644 worklenz-backend/src/public/assets/outline/yuque.js
create mode 100644 worklenz-backend/src/public/assets/outline/yuque.svg
create mode 100644 worklenz-backend/src/public/assets/outline/zhihu.js
create mode 100644 worklenz-backend/src/public/assets/outline/zhihu.svg
create mode 100644 worklenz-backend/src/public/assets/outline/zoom-in.js
create mode 100644 worklenz-backend/src/public/assets/outline/zoom-in.svg
create mode 100644 worklenz-backend/src/public/assets/outline/zoom-out.js
create mode 100644 worklenz-backend/src/public/assets/outline/zoom-out.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/.gitkeep
create mode 100644 worklenz-backend/src/public/assets/twotone/account-book.js
create mode 100644 worklenz-backend/src/public/assets/twotone/account-book.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/alert.js
create mode 100644 worklenz-backend/src/public/assets/twotone/alert.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/api.js
create mode 100644 worklenz-backend/src/public/assets/twotone/api.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/appstore.js
create mode 100644 worklenz-backend/src/public/assets/twotone/appstore.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/audio.js
create mode 100644 worklenz-backend/src/public/assets/twotone/audio.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/bank.js
create mode 100644 worklenz-backend/src/public/assets/twotone/bank.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/bell.js
create mode 100644 worklenz-backend/src/public/assets/twotone/bell.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/book.js
create mode 100644 worklenz-backend/src/public/assets/twotone/book.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/box-plot.js
create mode 100644 worklenz-backend/src/public/assets/twotone/box-plot.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/bug.js
create mode 100644 worklenz-backend/src/public/assets/twotone/bug.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/build.js
create mode 100644 worklenz-backend/src/public/assets/twotone/build.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/bulb.js
create mode 100644 worklenz-backend/src/public/assets/twotone/bulb.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/calculator.js
create mode 100644 worklenz-backend/src/public/assets/twotone/calculator.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/calendar.js
create mode 100644 worklenz-backend/src/public/assets/twotone/calendar.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/camera.js
create mode 100644 worklenz-backend/src/public/assets/twotone/camera.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/car.js
create mode 100644 worklenz-backend/src/public/assets/twotone/car.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/carry-out.js
create mode 100644 worklenz-backend/src/public/assets/twotone/carry-out.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/check-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/check-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/check-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/check-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/ci-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/ci-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/ci.js
create mode 100644 worklenz-backend/src/public/assets/twotone/ci.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/clock-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/clock-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/close-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/close-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/close-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/close-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/cloud.js
create mode 100644 worklenz-backend/src/public/assets/twotone/cloud.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/code.js
create mode 100644 worklenz-backend/src/public/assets/twotone/code.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/compass.js
create mode 100644 worklenz-backend/src/public/assets/twotone/compass.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/contacts.js
create mode 100644 worklenz-backend/src/public/assets/twotone/contacts.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/container.js
create mode 100644 worklenz-backend/src/public/assets/twotone/container.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/control.js
create mode 100644 worklenz-backend/src/public/assets/twotone/control.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/copy.js
create mode 100644 worklenz-backend/src/public/assets/twotone/copy.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/copyright-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/copyright-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/copyright.js
create mode 100644 worklenz-backend/src/public/assets/twotone/copyright.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/credit-card.js
create mode 100644 worklenz-backend/src/public/assets/twotone/credit-card.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/crown.js
create mode 100644 worklenz-backend/src/public/assets/twotone/crown.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/customer-service.js
create mode 100644 worklenz-backend/src/public/assets/twotone/customer-service.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/dashboard.js
create mode 100644 worklenz-backend/src/public/assets/twotone/dashboard.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/database.js
create mode 100644 worklenz-backend/src/public/assets/twotone/database.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/delete.js
create mode 100644 worklenz-backend/src/public/assets/twotone/delete.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/diff.js
create mode 100644 worklenz-backend/src/public/assets/twotone/diff.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/dislike.js
create mode 100644 worklenz-backend/src/public/assets/twotone/dislike.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/dollar-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/dollar-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/dollar.js
create mode 100644 worklenz-backend/src/public/assets/twotone/dollar.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/down-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/down-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/down-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/down-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/edit.js
create mode 100644 worklenz-backend/src/public/assets/twotone/edit.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/environment.js
create mode 100644 worklenz-backend/src/public/assets/twotone/environment.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/euro-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/euro-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/euro.js
create mode 100644 worklenz-backend/src/public/assets/twotone/euro.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/exclamation-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/exclamation-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/experiment.js
create mode 100644 worklenz-backend/src/public/assets/twotone/experiment.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/eye-invisible.js
create mode 100644 worklenz-backend/src/public/assets/twotone/eye-invisible.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/eye.js
create mode 100644 worklenz-backend/src/public/assets/twotone/eye.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-add.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-add.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-excel.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-excel.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-exclamation.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-exclamation.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-image.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-image.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-markdown.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-markdown.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-pdf.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-pdf.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-ppt.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-ppt.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-text.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-text.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-unknown.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-unknown.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-word.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-word.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file-zip.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file-zip.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/file.js
create mode 100644 worklenz-backend/src/public/assets/twotone/file.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/filter.js
create mode 100644 worklenz-backend/src/public/assets/twotone/filter.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/fire.js
create mode 100644 worklenz-backend/src/public/assets/twotone/fire.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/flag.js
create mode 100644 worklenz-backend/src/public/assets/twotone/flag.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/folder-add.js
create mode 100644 worklenz-backend/src/public/assets/twotone/folder-add.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/folder-open.js
create mode 100644 worklenz-backend/src/public/assets/twotone/folder-open.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/folder.js
create mode 100644 worklenz-backend/src/public/assets/twotone/folder.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/frown.js
create mode 100644 worklenz-backend/src/public/assets/twotone/frown.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/fund.js
create mode 100644 worklenz-backend/src/public/assets/twotone/fund.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/funnel-plot.js
create mode 100644 worklenz-backend/src/public/assets/twotone/funnel-plot.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/gift.js
create mode 100644 worklenz-backend/src/public/assets/twotone/gift.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/gold.js
create mode 100644 worklenz-backend/src/public/assets/twotone/gold.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/hdd.js
create mode 100644 worklenz-backend/src/public/assets/twotone/hdd.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/heart.js
create mode 100644 worklenz-backend/src/public/assets/twotone/heart.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/highlight.js
create mode 100644 worklenz-backend/src/public/assets/twotone/highlight.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/home.js
create mode 100644 worklenz-backend/src/public/assets/twotone/home.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/hourglass.js
create mode 100644 worklenz-backend/src/public/assets/twotone/hourglass.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/html5.js
create mode 100644 worklenz-backend/src/public/assets/twotone/html5.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/idcard.js
create mode 100644 worklenz-backend/src/public/assets/twotone/idcard.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/info-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/info-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/insurance.js
create mode 100644 worklenz-backend/src/public/assets/twotone/insurance.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/interaction.js
create mode 100644 worklenz-backend/src/public/assets/twotone/interaction.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/layout.js
create mode 100644 worklenz-backend/src/public/assets/twotone/layout.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/left-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/left-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/left-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/left-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/like.js
create mode 100644 worklenz-backend/src/public/assets/twotone/like.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/lock.js
create mode 100644 worklenz-backend/src/public/assets/twotone/lock.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/mail.js
create mode 100644 worklenz-backend/src/public/assets/twotone/mail.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/medicine-box.js
create mode 100644 worklenz-backend/src/public/assets/twotone/medicine-box.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/meh.js
create mode 100644 worklenz-backend/src/public/assets/twotone/meh.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/message.js
create mode 100644 worklenz-backend/src/public/assets/twotone/message.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/minus-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/minus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/minus-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/minus-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/mobile.js
create mode 100644 worklenz-backend/src/public/assets/twotone/mobile.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/money-collect.js
create mode 100644 worklenz-backend/src/public/assets/twotone/money-collect.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/notification.js
create mode 100644 worklenz-backend/src/public/assets/twotone/notification.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/pause-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/pause-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/phone.js
create mode 100644 worklenz-backend/src/public/assets/twotone/phone.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/picture.js
create mode 100644 worklenz-backend/src/public/assets/twotone/picture.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/pie-chart.js
create mode 100644 worklenz-backend/src/public/assets/twotone/pie-chart.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/play-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/play-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/play-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/play-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/plus-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/plus-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/plus-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/plus-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/pound-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/pound-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/printer.js
create mode 100644 worklenz-backend/src/public/assets/twotone/printer.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/profile.js
create mode 100644 worklenz-backend/src/public/assets/twotone/profile.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/project.js
create mode 100644 worklenz-backend/src/public/assets/twotone/project.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/property-safety.js
create mode 100644 worklenz-backend/src/public/assets/twotone/property-safety.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/pushpin.js
create mode 100644 worklenz-backend/src/public/assets/twotone/pushpin.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/question-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/question-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/reconciliation.js
create mode 100644 worklenz-backend/src/public/assets/twotone/reconciliation.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/red-envelope.js
create mode 100644 worklenz-backend/src/public/assets/twotone/red-envelope.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/rest.js
create mode 100644 worklenz-backend/src/public/assets/twotone/rest.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/right-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/right-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/right-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/right-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/rocket.js
create mode 100644 worklenz-backend/src/public/assets/twotone/rocket.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/safety-certificate.js
create mode 100644 worklenz-backend/src/public/assets/twotone/safety-certificate.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/save.js
create mode 100644 worklenz-backend/src/public/assets/twotone/save.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/schedule.js
create mode 100644 worklenz-backend/src/public/assets/twotone/schedule.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/security-scan.js
create mode 100644 worklenz-backend/src/public/assets/twotone/security-scan.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/setting.js
create mode 100644 worklenz-backend/src/public/assets/twotone/setting.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/shop.js
create mode 100644 worklenz-backend/src/public/assets/twotone/shop.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/shopping.js
create mode 100644 worklenz-backend/src/public/assets/twotone/shopping.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/skin.js
create mode 100644 worklenz-backend/src/public/assets/twotone/skin.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/sliders.js
create mode 100644 worklenz-backend/src/public/assets/twotone/sliders.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/smile.js
create mode 100644 worklenz-backend/src/public/assets/twotone/smile.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/snippets.js
create mode 100644 worklenz-backend/src/public/assets/twotone/snippets.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/sound.js
create mode 100644 worklenz-backend/src/public/assets/twotone/sound.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/star.js
create mode 100644 worklenz-backend/src/public/assets/twotone/star.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/stop.js
create mode 100644 worklenz-backend/src/public/assets/twotone/stop.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/switcher.js
create mode 100644 worklenz-backend/src/public/assets/twotone/switcher.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/tablet.js
create mode 100644 worklenz-backend/src/public/assets/twotone/tablet.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/tag.js
create mode 100644 worklenz-backend/src/public/assets/twotone/tag.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/tags.js
create mode 100644 worklenz-backend/src/public/assets/twotone/tags.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/thunderbolt.js
create mode 100644 worklenz-backend/src/public/assets/twotone/thunderbolt.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/tool.js
create mode 100644 worklenz-backend/src/public/assets/twotone/tool.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/trademark-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/trademark-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/trophy.js
create mode 100644 worklenz-backend/src/public/assets/twotone/trophy.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/unlock.js
create mode 100644 worklenz-backend/src/public/assets/twotone/unlock.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/up-circle.js
create mode 100644 worklenz-backend/src/public/assets/twotone/up-circle.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/up-square.js
create mode 100644 worklenz-backend/src/public/assets/twotone/up-square.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/usb.js
create mode 100644 worklenz-backend/src/public/assets/twotone/usb.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/video-camera.js
create mode 100644 worklenz-backend/src/public/assets/twotone/video-camera.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/wallet.js
create mode 100644 worklenz-backend/src/public/assets/twotone/wallet.svg
create mode 100644 worklenz-backend/src/public/assets/twotone/warning.js
create mode 100644 worklenz-backend/src/public/assets/twotone/warning.svg
create mode 100644 worklenz-backend/src/public/favicon.ico
create mode 100644 worklenz-backend/src/public/main.91ef34cb24678df1.js
create mode 100644 worklenz-backend/src/public/manifest.webmanifest
create mode 100644 worklenz-backend/src/public/ngsw-worker.js
create mode 100644 worklenz-backend/src/public/ngsw.json
create mode 100644 worklenz-backend/src/public/polyfills.f07f6ddac0a4feb2.js
create mode 100644 worklenz-backend/src/public/runtime.9997dc39514a207d.js
create mode 100644 worklenz-backend/src/public/safety-worker.js
create mode 100644 worklenz-backend/src/public/styles.c78b93a1a5b19d7f.css
create mode 100644 worklenz-backend/src/public/worker-basic.min.js
create mode 100644 worklenz-backend/src/redis/client.ts
create mode 100644 worklenz-backend/src/routes/apis/activity-logs-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/admin-center-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/attachments-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/clients-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/gannt-apis/roadmap-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/gannt-apis/schedule-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/gannt-apis/workload-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/gantt-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/home-page-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/index.ts
create mode 100644 worklenz-backend/src/routes/apis/job-titles-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/labels-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/notifications-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/personal-overview-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/priorities-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-categories-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-comments-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-folders-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-healths-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-insights-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-managers-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-members-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-statuses-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/project-templates-api.ts
create mode 100644 worklenz-backend/src/routes/apis/projects-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/pt-statuses-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/pt-tasks-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/pt_task-phases-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/reporting-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/reporting-export-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/resource-allocation-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/settings-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/shared-projects-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/statuses-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/sub-tasks-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/task-comments-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/task-phases-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/task-templates-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/task-work-log-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/tasks-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/team-members-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/teams-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/timezones-api-router.ts
create mode 100644 worklenz-backend/src/routes/apis/todo-list-api-router.ts
create mode 100644 worklenz-backend/src/routes/auth/index.ts
create mode 100644 worklenz-backend/src/routes/email-templates.ts
create mode 100644 worklenz-backend/src/routes/index.ts
create mode 100644 worklenz-backend/src/routes/public/index.ts
create mode 100644 worklenz-backend/src/services/activity-logs/activity-logs.service.ts
create mode 100644 worklenz-backend/src/services/activity-logs/interfaces.ts
create mode 100644 worklenz-backend/src/services/notifications/email-notifications.service.ts
create mode 100644 worklenz-backend/src/services/notifications/interfaces.ts
create mode 100644 worklenz-backend/src/services/notifications/messages/TaskAdd.ts
create mode 100644 worklenz-backend/src/services/notifications/messages/TaskRemove.ts
create mode 100644 worklenz-backend/src/services/notifications/notification-types.ts
create mode 100644 worklenz-backend/src/services/notifications/notification.ts
create mode 100644 worklenz-backend/src/services/notifications/notifications.service.ts
create mode 100644 worklenz-backend/src/shared/constants.ts
create mode 100644 worklenz-backend/src/shared/constraints.ts
create mode 100644 worklenz-backend/src/shared/csp.ts
create mode 100644 worklenz-backend/src/shared/email-notifications.ts
create mode 100644 worklenz-backend/src/shared/email-templates.ts
create mode 100644 worklenz-backend/src/shared/email.ts
create mode 100644 worklenz-backend/src/shared/file-constants.ts
create mode 100644 worklenz-backend/src/shared/io.ts
create mode 100644 worklenz-backend/src/shared/password-strength-check.ts
create mode 100644 worklenz-backend/src/shared/postgresql-error-codes.json
create mode 100644 worklenz-backend/src/shared/s3.ts
create mode 100644 worklenz-backend/src/shared/safe-controller-function.ts
create mode 100644 worklenz-backend/src/shared/slack.ts
create mode 100644 worklenz-backend/src/shared/tasks-controller-utils.ts
create mode 100644 worklenz-backend/src/shared/utils.ts
create mode 100644 worklenz-backend/src/socket.io/commands/.gitkeep
create mode 100644 worklenz-backend/src/socket.io/commands/on-connect.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-create-label.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-create-project-category.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-disconnect.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-get-task-progress.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-join-or-leave-project-room.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-phase-end-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-phase-start-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-category-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-end-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-health-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-start-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-status-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-project-subscriber-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-quick-assign-or-remove.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-quick-task.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-roadmap-sort-order-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-description-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-end-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-labels-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-name-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-phase-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-priority-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-sort-order-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-start-date-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-status-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-subscriber-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-timer-start.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-task-timer-stop.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on-time-estimation-change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_gannt_drag_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_create_label.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_name_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_quick_task.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_description_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_end_date_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_labels_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_name_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_phase_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_priority_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_sort_order_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_start_date_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_status_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_pt_task_time_estimation_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_schedule_member_allocation_create.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_schedule_member_end_date_change.ts
create mode 100644 worklenz-backend/src/socket.io/commands/on_schedule_member_start_date_change.ts
create mode 100644 worklenz-backend/src/socket.io/events.ts
create mode 100644 worklenz-backend/src/socket.io/index.ts
create mode 100644 worklenz-backend/src/socket.io/util.ts
create mode 100644 worklenz-backend/src/tests/.gitkeep
create mode 100644 worklenz-backend/src/tests/constants.spec.ts
create mode 100644 worklenz-backend/src/tests/test.spec.ts
create mode 100644 worklenz-backend/src/typings.d.ts
create mode 100644 worklenz-backend/src/utils/generate-project-key.ts
create mode 100644 worklenz-backend/src/utils/logger.ts
create mode 100644 worklenz-backend/src/views/_footer.pug
create mode 100644 worklenz-backend/src/views/_header.pug
create mode 100644 worklenz-backend/src/views/_hubspot.pug
create mode 100644 worklenz-backend/src/views/_scripts.pug
create mode 100644 worklenz-backend/src/views/admin/_fonts.pug
create mode 100644 worklenz-backend/src/views/admin/_maintenance.pug
create mode 100644 worklenz-backend/src/views/admin/_styles.pug
create mode 100644 worklenz-backend/src/views/admin/index.pug
create mode 100644 worklenz-backend/src/views/admin/layout.pug
create mode 100644 worklenz-backend/src/views/error/index.pug
create mode 100644 worklenz-backend/src/views/error/layout.pug
create mode 100644 worklenz-backend/src/views/index.pug
create mode 100644 worklenz-backend/src/views/layout.pug
create mode 100644 worklenz-backend/src/views/privacy-policy.pug
create mode 100644 worklenz-backend/src/views/terms-of-use.pug
create mode 100644 worklenz-backend/tsconfig.json
create mode 100644 worklenz-backend/tsconfig.prod.json
create mode 100644 worklenz-backend/worklenz-email-templates/admin-new-subscriber-notification.html
create mode 100644 worklenz-backend/worklenz-email-templates/deprecation-notice.html
create mode 100644 worklenz-backend/worklenz-email-templates/email-notifications/daily-digest.pug
create mode 100644 worklenz-backend/worklenz-email-templates/email-notifications/project-daily-digest.pug
create mode 100644 worklenz-backend/worklenz-email-templates/email-notifications/task-assignee-change.pug
create mode 100644 worklenz-backend/worklenz-email-templates/email-notifications/task-comment.pug
create mode 100644 worklenz-backend/worklenz-email-templates/email-notifications/task-moved-to-done.pug
create mode 100644 worklenz-backend/worklenz-email-templates/maintenance-template.html
create mode 100644 worklenz-backend/worklenz-email-templates/otp-verfication-code.html
create mode 100644 worklenz-backend/worklenz-email-templates/password-changed-notification.html
create mode 100644 worklenz-backend/worklenz-email-templates/reset-password.html
create mode 100644 worklenz-backend/worklenz-email-templates/team-invitation.html
create mode 100644 worklenz-backend/worklenz-email-templates/unregistered-team-invitation-notification.html
create mode 100644 worklenz-backend/worklenz-email-templates/welcome.html
create mode 100644 worklenz-frontend/.editorconfig
create mode 100644 worklenz-frontend/.eslintrc.json
create mode 100644 worklenz-frontend/.gitignore
create mode 100644 worklenz-frontend/.npmrc
create mode 100644 worklenz-frontend/Dockerfile
create mode 100644 worklenz-frontend/README.md
create mode 100644 worklenz-frontend/angular.json
create mode 100644 worklenz-frontend/karma.conf.js
create mode 100644 worklenz-frontend/ngsw-config.json
create mode 100644 worklenz-frontend/package-lock.json
create mode 100644 worklenz-frontend/package.json
create mode 100644 worklenz-frontend/proxy.config.json
create mode 100644 worklenz-frontend/src/app/DTOs/.gitkeep
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup/account-setup.component.html
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup/account-setup.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup/account-setup.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/account-setup/account-setup.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/teams-list/teams-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/teams-list/teams-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/teams-list/teams-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/account-setup/teams-list/teams-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/admin-center-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/admin-center-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/admin-center.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/layout/layout.component.html
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/layout/layout.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/layout/layout.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/layout/layout.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/overview/overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/overview/overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/overview/overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/overview/overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/sidebar/sidebar.component.html
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/sidebar/sidebar.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/sidebar/sidebar.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/sidebar/sidebar.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/teams/teams.component.html
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/teams/teams.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/teams/teams.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/teams/teams.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/users/users.component.html
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/users/users.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/users/users.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/admin-center/users/users.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/administrator-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/administrator.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/avatars/avatars.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/avatars/avatars.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/avatars/avatars.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/avatars/avatars.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/clients-autocomplete/clients-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/clients-autocomplete/clients-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/clients-autocomplete/clients-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/clients-autocomplete/clients-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/convert-to-subtask-modal/convert-to-subtask-modal.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/convert-to-subtask-modal/convert-to-subtask-modal.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/convert-to-subtask-modal/convert-to-subtask-modal.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/convert-to-subtask-modal/convert-to-subtask-modal.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/import-tasks-template/import-tasks-template.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/import-tasks-template/import-tasks-template.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/import-tasks-template/import-tasks-template.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/import-tasks-template/import-tasks-template.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/job-titles-autocomplete/job-titles-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/job-titles-autocomplete/job-titles-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/job-titles-autocomplete/job-titles-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/job-titles-autocomplete/job-titles-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/na/na.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/na/na.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/na/na.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/na/na.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/personal-overview/personal-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/personal-overview/personal-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/personal-overview/personal-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/personal-overview/personal-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-categories-autocomplete/project-categories-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-categories-autocomplete/project-categories-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-categories-autocomplete/project-categories-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-categories-autocomplete/project-categories-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-folders-autocomplete/project-folders-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-folders-autocomplete/project-folders-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-folders-autocomplete/project-folders-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-folders-autocomplete/project-folders-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-form-modal/project-form-modal.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-form-modal/project-form-modal.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-form-modal/project-form-modal.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-form-modal/project-form-modal.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-members-form/project-members-form.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-members-form/project-members-form.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-members-form/project-members-form.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-members-form/project-members-form.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-create-drawer/project-template-create-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-create-drawer/project-template-create-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-create-drawer/project-template-create-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-create-drawer/project-template-create-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/custom-template-list/custom-template-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/custom-template-list/custom-template-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/custom-template-list/custom-template-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/custom-template-list/custom-template-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/project-template-import-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/project-template-import-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/project-template-import-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/project-template-import-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/worklenz-template-list/worklenz-template-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/worklenz-template-list/worklenz-template-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/worklenz-template-list/worklenz-template-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-template-import-drawer/worklenz-template-list/worklenz-template-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-drawer/project-updates-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-drawer/project-updates-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-drawer/project-updates-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-drawer/project-updates-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-input/project-updates-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-input/project-updates-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-input/project-updates-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-input/project-updates-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-list/project-updates-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-list/project-updates-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-list/project-updates-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/project-updates-list/project-updates-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/projects-autocomplete/projects-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/projects-autocomplete/projects-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/projects-autocomplete/projects-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/projects-autocomplete/projects-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/resource-gantt/resource-gantt.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/resource-gantt/resource-gantt.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/resource-gantt/resource-gantt.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/resource-gantt/resource-gantt.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/status-form/status-form.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/status-form/status-form.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/status-form/status-form.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/status-form/status-form.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-priority-label/task-priority-label.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-priority-label/task-priority-label.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-priority-label/task-priority-label.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-priority-label/task-priority-label.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-template-drawer/task-template-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-template-drawer/task-template-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-template-drawer/task-template-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-template-drawer/task-template-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/interfaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/task-timer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/task-timer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/task-timer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/task-timer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-timer/task-timer.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/interfaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-activity-log/task-view-activity-log.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-activity-log/task-view-activity-log.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-activity-log/task-view-activity-log.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-activity-log/task-view-activity-log.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-assignees/task-view-assignees.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-assignees/task-view-assignees.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-assignees/task-view-assignees.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-assignees/task-view-assignees.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments-thumb/task-view-attachments-thumb.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments-thumb/task-view-attachments-thumb.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments-thumb/task-view-attachments-thumb.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments-thumb/task-view-attachments-thumb.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-attachments/task-view-attachments.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments-input/task-view-comments-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments-input/task-view-comments-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments-input/task-view-comments-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments-input/task-view-comments-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments/task-view-comments.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments/task-view-comments.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments/task-view-comments.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-comments/task-view-comments.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-description/task-view-description.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-description/task-view-description.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-description/task-view-description.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-description/task-view-description.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-due-date/task-view-due-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-due-date/task-view-due-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-due-date/task-view-due-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-due-date/task-view-due-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-estimation/task-view-estimation.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-estimation/task-view-estimation.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-estimation/task-view-estimation.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-estimation/task-view-estimation.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-info/task-view-info.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-info/task-view-info.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-info/task-view-info.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-info/task-view-info.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-labels/task-view-labels.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-labels/task-view-labels.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-labels/task-view-labels.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-labels/task-view-labels.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-name/task-view-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-name/task-view-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-name/task-view-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-name/task-view-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-notify-to-user/task-view-notify-to-user.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-notify-to-user/task-view-notify-to-user.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-notify-to-user/task-view-notify-to-user.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-notify-to-user/task-view-notify-to-user.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-phase/task-view-phase.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-phase/task-view-phase.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-phase/task-view-phase.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-phase/task-view-phase.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-priority/task-view-priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-priority/task-view-priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-priority/task-view-priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-priority/task-view-priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-sub-tasks/task-view-sub-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-sub-tasks/task-view-sub-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-sub-tasks/task-view-sub-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-sub-tasks/task-view-sub-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-time-log/task-view-time-log.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-time-log/task-view-time-log.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-time-log/task-view-time-log.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view-time-log/task-view-time-log.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.service.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/task-view/task-view.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-group-view/tasks-group-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-group-view/tasks-group-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-group-view/tasks-group-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-group-view/tasks-group-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-progress-bar/tasks-progress-bar.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-progress-bar/tasks-progress-bar.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-progress-bar/tasks-progress-bar.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-progress-bar/tasks-progress-bar.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/tasks-progress-bar/with-percentage-mark.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-autocomplete/team-members-autocomplete.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-autocomplete/team-members-autocomplete.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-autocomplete/team-members-autocomplete.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-autocomplete/team-members-autocomplete.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-form/team-members-form.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-form/team-members-form.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-form/team-members-form.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/team-members-form/team-members-form.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/toggle-menu-button/toggle-menu-button.component.html
create mode 100644 worklenz-frontend/src/app/administrator/components/toggle-menu-button/toggle-menu-button.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/components/toggle-menu-button/toggle-menu-button.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/components/toggle-menu-button/toggle-menu-button.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/alerts/alerts.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/alerts/alerts.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/alerts/alerts.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/alerts/alerts.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/header/header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/header/header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/header/header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/header/header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/layout.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/layout.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/layout.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/layout.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/licensing-alerts/licensing-alerts.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/licensing-alerts/licensing-alerts.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/licensing-alerts/licensing-alerts.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/licensing-alerts/licensing-alerts.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notification-template/notification-template.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notification-template/notification-template.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notification-template/notification-template.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notification-template/notification-template.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notifications-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notifications-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notifications-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/notifications-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/tag-background.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/layout/notifications-drawer/types.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-card/task-card.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-card/task-card.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-card/task-card.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-card/task-card.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-creation-assignees/task-creation-assignees.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-creation-assignees/task-creation-assignees.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-creation-assignees/task-creation-assignees.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-creation-assignees/task-creation-assignees.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-end-date/task-end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-end-date/task-end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-end-date/task-end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-end-date/task-end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-labels/task-labels.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-labels/task-labels.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-labels/task-labels.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-labels/task-labels.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-members/task-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-members/task-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-members/task-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-members/task-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-name/task-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-name/task-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-name/task-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-name/task-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-priority/task-priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-priority/task-priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-priority/task-priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-priority/task-priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-progress/task-progress.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-progress/task-progress.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-progress/task-progress.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-progress/task-progress.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-subtask-count/task-subtask-count.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-subtask-count/task-subtask-count.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-subtask-count/task-subtask-count.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/components/task-subtask-count/task-subtask-count.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/kanban-board.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/kanban-board.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/kanban-board.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/kanban-board.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/models/board.model.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/models/column.model.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-board/pipes/validate-min-date.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-hash-map-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-view-v2-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-view-v2.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/kanban-view-v2/kanban-view-v2.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/gantt-chart-v2-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/gantt-chart-v2.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-input/add-task-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-input/add-task-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-input/add-task-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-input/add-task-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-row/add-task-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-row/add-task-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-row/add-task-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/add-task-row/add-task-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/end-date/end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/end-date/end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/end-date/end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/end-date/end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/filters/filters.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/filters/filters.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/filters/filters.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/filters/filters.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/start-date/start-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/start-date/start-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/start-date/start-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/start-date/start-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-bar/task-bar.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-bar/task-bar.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-bar/task-bar.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-bar/task-bar.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-name/task-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-name/task-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-name/task-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/components/task-name/task-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/directives/drag-move.directive.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/project-roadmap-v2-custom.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/project-roadmap-v2-custom.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/project-roadmap-v2-custom.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/project-roadmap-v2-custom.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/services/roadmap-v2-hashmap.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/roadmap-v2/project-roadmap-v2-custom/services/roadmap-v2-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/interfaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/pipes/end-name-check.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-columns-toggle/task-list-columns-toggle.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-columns-toggle/task-list-columns-toggle.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-columns-toggle/task-list-columns-toggle.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-columns-toggle/task-list-columns-toggle.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-filters/task-list-filters.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-filters/task-list-filters.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-filters/task-list-filters.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-filters/task-list-filters.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-hash-map.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-add-task-input/task-list-add-task-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-add-task-input/task-list-add-task-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-add-task-input/task-list-add-task-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-add-task-input/task-list-add-task-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-bulk-actions/task-list-bulk-actions.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-bulk-actions/task-list-bulk-actions.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-bulk-actions/task-list-bulk-actions.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-bulk-actions/task-list-bulk-actions.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/interfaces/convert-subtask-request.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/subtask-convert-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/task-list-context-menu.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/task-list-context-menu.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/task-list-context-menu.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-context-menu/task-list-context-menu.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-group-settings/task-list-group-settings.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-group-settings/task-list-group-settings.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-group-settings/task-list-group-settings.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-group-settings/task-list-group-settings.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-header/task-list-header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-header/task-list-header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-header/task-list-header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-header/task-list-header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-duration/task-list-phase-duration.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-duration/task-list-phase-duration.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-duration/task-list-phase-duration.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-duration/task-list-phase-duration.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-settings-drawer/task-list-phase-settings-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-settings-drawer/task-list-phase-settings-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-settings-drawer/task-list-phase-settings-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-phase-settings-drawer/task-list-phase-settings-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/ellipsis-tooltip-title.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/sub-tasks-arrow-color.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/sub-tasks-arrow-icon.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/truncate-if-long.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/validate-max-date.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/pipes/validate-min-date.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-description/task-list-description.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-description/task-list-description.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-description/task-list-description.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-description/task-list-description.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-end-date/task-list-end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-end-date/task-list-end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-end-date/task-list-end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-end-date/task-list-end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-labels/task-list-labels.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-labels/task-list-labels.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-labels/task-list-labels.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-labels/task-list-labels.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-members/task-list-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-members/task-list-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-members/task-list-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-members/task-list-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-phase/task-list-phase.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-phase/task-list-phase.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-phase/task-list-phase.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-phase/task-list-phase.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-priority/task-list-priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-priority/task-list-priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-priority/task-list-priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-priority/task-list-priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-start-date/task-list-start-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-start-date/task-list-start-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-start-date/task-list-start-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-start-date/task-list-start-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-status/task-list-status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-status/task-list-status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-status/task-list-status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-status/task-list-status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-timer/task-list-timer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-timer/task-list-timer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-timer/task-list-timer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-list-timer/task-list-timer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-progress/task-progress.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-progress/task-progress.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-progress/task-progress.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-row/task-progress/task-progress.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-table.component.html
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-table.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-table.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-table/task-list-table.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-v2-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-v2.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/modules/task-list-v2/task-list-v2.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/activity-log/activity-log.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/activity-log/activity-log.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/activity-log/activity-log.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/activity-log/activity-log.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/dashboard.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/dashboard.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/dashboard.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/dashboard.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-projects/my-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-projects/my-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-projects/my-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-projects/my-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-done/task-done.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-done/task-done.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-done/task-done.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-done/task-done.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-due-date/task-due-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-due-date/task-due-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-due-date/task-due-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-due-date/task-due-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-name/task-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-name/task-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-name/task-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-name/task-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-project/task-project.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-project/task-project.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-project/task-project.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-project/task-project.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-status/task-status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-status/task-status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-status/task-status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/task-status/task-status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/task-add-container/task-add-container.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/task-add-container/task-add-container.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/task-add-container/task-add-container.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/task-add-container/task-add-container.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/tasks-table.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/tasks-table.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/tasks-table.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/components/tasks-table/tasks-table.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/my-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/my-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/my-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/my-tasks/my-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/personal-tasks/personal-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/personal-tasks/personal-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/personal-tasks/personal-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/dashboard/personal-tasks/personal-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/homepage-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/homepage.enum.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/intefaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/my-dashboard-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/my-dashboard.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/personal-todo-list/personal-todo-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/personal-todo-list/personal-todo-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/personal-todo-list/personal-todo-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/personal-todo-list/personal-todo-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/projects-tasks/projects-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/projects-tasks/projects-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/projects-tasks/projects-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/my-dashboard/projects-tasks/projects-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/all-tasks-attachments/all-tasks-attachments.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/all-tasks-attachments/all-tasks-attachments.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/all-tasks-attachments/all-tasks-attachments.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/all-tasks-attachments/all-tasks-attachments.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/context-menu/context-menu.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/context-menu/context-menu.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/context-menu/context-menu.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/context-menu/context-menu.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/end-date/end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/end-date/end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/end-date/end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/end-date/end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-task-add-container/member-task-add-container.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-task-add-container/member-task-add-container.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-task-add-container/member-task-add-container.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-task-add-container/member-task-add-container.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/member-tasks-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/member-tasks-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/member-tasks-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/member-tasks-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/overview-tab/overview-tab.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/overview-tab/overview-tab.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/overview-tab/overview-tab.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/member-tasks-drawer/overview-tab/overview-tab.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/phase/phase.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/phase/phase.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/phase/phase.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/phase/phase.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/priority/priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/priority/priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/priority/priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/priority/priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/start-date/start-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/start-date/start-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/start-date/start-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/start-date/start-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/status/status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/status/status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/status/status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/status/status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-header/task-list-header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-header/task-list-header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-header/task-list-header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-header/task-list-header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-row/task-list-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-row/task-list-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-row/task-list-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-list-row/task-list-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-name/task-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-name/task-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-name/task-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/components/task-name/task-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/services/wl-tasks-hash-map.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/services/wl-tasks.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/workload-gaant-chart-v2.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/workload-gaant-chart-v2.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/workload-gaant-chart-v2.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/components-v2/workload-gaant-chart-v2/workload-gaant-chart-v2.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/components/project-stats/project-stats.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/components/project-stats/project-stats.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/components/project-stats/project-stats.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/components/project-stats/project-stats.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/last-updated-tasks/last-updated-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/last-updated-tasks/last-updated-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/last-updated-tasks/last-updated-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/last-updated-tasks/last-updated-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/priority-breakdown/priority-breakdown.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/priority-breakdown/priority-breakdown.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/priority-breakdown/priority-breakdown.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/priority-breakdown/priority-breakdown.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/project-deadline/project-deadline.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/project-deadline/project-deadline.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/project-deadline/project-deadline.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/project-deadline/project-deadline.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/status-overview/status-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/status-overview/status-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/status-overview/status-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-insights/status-overview/status-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-stats/member-stats.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-stats/member-stats.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-stats/member-stats.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-stats/member-stats.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-tasks/member-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-tasks/member-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-tasks/member-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/member-tasks/member-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/project-insights-member-overview/project-insights-member-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/project-insights-member-overview/project-insights-member-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/project-insights-member-overview/project-insights-member-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-members/project-insights-member-overview/project-insights-member-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-overview/project-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-overview/project-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-overview/project-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/project-overview/project-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/task-insights/task-insights.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/task-insights/task-insights.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/task-insights/task-insights.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-insights/task-insights/task-insights.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-members/project-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-members/project-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-members/project-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-members/project-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-updates/project-updates.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-updates/project-updates.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-updates/project-updates.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-updates/project-updates.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-sharing/project-sharing-functions.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-sharing/template.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view-extra/project-view-extra.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view-extra/project-view-extra.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view-extra/project-view-extra.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view-extra/project-view-extra.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/project-view/project-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects.service.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-member-allocations/migrate-member-allocations.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-member-allocations/migrate-member-allocations.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-member-allocations/migrate-member-allocations.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-member-allocations/migrate-member-allocations.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-phase-sort-order/migrate-phase-sort-order.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-phase-sort-order/migrate-phase-sort-order.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-phase-sort-order/migrate-phase-sort-order.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-phase-sort-order/migrate-phase-sort-order.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-project-phases/migrate-project-phases.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-project-phases/migrate-project-phases.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-project-phases/migrate-project-phases.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-project-phases/migrate-project-phases.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-templates/migrate-templates.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-templates/migrate-templates.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-templates/migrate-templates.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/migrate-templates/migrate-templates.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/pipes/project-filter-by-tooltip.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/pipes/project-filter-by-tooltip.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-form-drawer/projects-folder-form-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-form-drawer/projects-folder-form-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-form-drawer/projects-folder-form-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-form-drawer/projects-folder-form-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-form-drawer/projects-folder-form-drawer.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-view/projects-folder-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-view/projects-folder-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-view/projects-folder-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-folder-view/projects-folder-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-list-view/projects-list-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-list-view/projects-list-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-list-view/projects-list-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects-list-view/projects-list-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/projects/projects/projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/estimated-vs-actual-chart/estimated-vs-actual-chart.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/estimated-vs-actual-chart/estimated-vs-actual-chart.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/estimated-vs-actual-chart/estimated-vs-actual-chart.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/estimated-vs-actual-chart/estimated-vs-actual-chart.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/member-logs-breakdown/member-logs-breakdown.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/member-logs-breakdown/member-logs-breakdown.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/member-logs-breakdown/member-logs-breakdown.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/member-logs-breakdown/member-logs-breakdown.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-category/project-category.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-category/project-category.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-category/project-category.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-category/project-category.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-health/project-health.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-health/project-health.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-health/project-health.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-health/project-health.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-logs-breakdown/project-logs-breakdown.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-logs-breakdown/project-logs-breakdown.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-logs-breakdown/project-logs-breakdown.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-logs-breakdown/project-logs-breakdown.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-start-end-dates/project-start-end-dates.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-start-end-dates/project-start-end-dates.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-start-end-dates/project-start-end-dates.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-start-end-dates/project-start-end-dates.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-status/project-status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-status/project-status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-status/project-status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/project-status/project-status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-header/rpt-header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-header/rpt-header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-header/rpt-header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-header/rpt-header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-layout/rpt-layout.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-layout/rpt-layout.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-layout/rpt-layout.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/components/rpt-layout/rpt-layout.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-drawer-title/rpt-drawer-title.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-drawer-title/rpt-drawer-title.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-drawer-title/rpt-drawer-title.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-drawer-title/rpt-drawer-title.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-flat-task-list/rpt-flat-task-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-flat-task-list/rpt-flat-task-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-flat-task-list/rpt-flat-task-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-flat-task-list/rpt-flat-task-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-flat-task-list/rpt-flat-tasks-list.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-grouped-task-list/rpt-grouped-task-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-grouped-task-list/rpt-grouped-task-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-grouped-task-list/rpt-grouped-task-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-grouped-task-list/rpt-grouped-task-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-grouped-task-list/rpt-grouped-task-list.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-member-projects-list/rpt-member-projects-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-member-projects-list/rpt-member-projects-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-member-projects-list/rpt-member-projects-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-member-projects-list/rpt-member-projects-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-projects-list/rpt-projects-list.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-projects-list/rpt-projects-list.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-projects-list/rpt-projects-list.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-projects-list/rpt-projects-list.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/common/rpt-projects-list/rpt-projects-list.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/reporting-drawers.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-overview/rpt-member-drawer-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-overview/rpt-member-drawer-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-overview/rpt-member-drawer-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-overview/rpt-member-drawer-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-projects/rpt-member-drawer-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-projects/rpt-member-drawer-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-projects/rpt-member-drawer-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-projects/rpt-member-drawer-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-tasks/rpt-member-drawer-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-tasks/rpt-member-drawer-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-tasks/rpt-member-drawer-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer-tasks/rpt-member-drawer-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-member-drawer/rpt-member-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-members/rpt-project-drawer-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-members/rpt-project-drawer-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-members/rpt-project-drawer-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-members/rpt-project-drawer-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-overview/rpt-project-drawer-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-overview/rpt-project-drawer-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-overview/rpt-project-drawer-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-overview/rpt-project-drawer-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-tasks/rpt-project-drawer-tasks.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-tasks/rpt-project-drawer-tasks.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-tasks/rpt-project-drawer-tasks.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer-tasks/rpt-project-drawer-tasks.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-project-drawer/rpt-project-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/activity-logs/activity-logs.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/activity-logs/activity-logs.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/activity-logs/activity-logs.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/activity-logs/activity-logs.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/duration-filter/duration-filter.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/duration-filter/duration-filter.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/duration-filter/duration-filter.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/duration-filter/duration-filter.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/rpt-single-member-drawer-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/rpt-single-member-drawer-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/rpt-single-member-drawer-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/rpt-single-member-drawer-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/service/log-header.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/time-logs/single-member-time-logs.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/time-logs/single-member-time-logs.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/time-logs/single-member-time-logs.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer-overview/time-logs/single-member-time-logs.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-drawer/rpt-single-member-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-projects-drawer/rpt-single-member-projects-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-projects-drawer/rpt-single-member-projects-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-projects-drawer/rpt-single-member-projects-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-projects-drawer/rpt-single-member-projects-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-stat/rpt-single-member-stat.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-stat/rpt-single-member-stat.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-stat/rpt-single-member-stat.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-single-member-stat/rpt-single-member-stat.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-task-view-drawer/rpt-task-view-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-task-view-drawer/rpt-task-view-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-task-view-drawer/rpt-task-view-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-task-view-drawer/rpt-task-view-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-task-view-drawer/rpt-task-view-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-tasks-drawer/rpt-tasks-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-tasks-drawer/rpt-tasks-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-tasks-drawer/rpt-tasks-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-tasks-drawer/rpt-tasks-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-tasks-drawer/rpt-tasks-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-members/rpt-team-drawer-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-members/rpt-team-drawer-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-members/rpt-team-drawer-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-members/rpt-team-drawer-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-projects/rpt-team-drawer-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-projects/rpt-team-drawer-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-projects/rpt-team-drawer-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer-projects/rpt-team-drawer-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-drawer.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-overview/rpt-team-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-overview/rpt-team-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-overview/rpt-team-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/drawers/rpt-team-drawer/rpt-team-overview/rpt-team-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/interfaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation/rpt-allocation.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation/rpt-allocation.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation/rpt-allocation.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-allocation/rpt-allocation/rpt-allocation.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members/rpt-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members/rpt-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members/rpt-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-members/rpt-members/rpt-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview-cards/rpt-overview-cards.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview-cards/rpt-overview-cards.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview-cards/rpt-overview-cards.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview-cards/rpt-overview-cards.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-overview/rpt-overview/rpt-overview.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects-estimated-vs-actual/rpt-projects-estimated-vs-actual.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects-estimated-vs-actual/rpt-projects-estimated-vs-actual.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects-estimated-vs-actual/rpt-projects-estimated-vs-actual.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects-estimated-vs-actual/rpt-projects-estimated-vs-actual.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-projects/rpt-projects/rpt-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/rpt-time-estimation-vs-actual-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/rpt-time-estimation-vs-actual.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/time-estimation-vs-actual-projects/time-estimation-vs-actual-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/time-estimation-vs-actual-projects/time-estimation-vs-actual-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/time-estimation-vs-actual-projects/time-estimation-vs-actual-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-estimation-vs-actual/time-estimation-vs-actual-projects/time-estimation-vs-actual-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/rpt-time-members-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/rpt-time-members.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/time-members/time-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/time-members/time-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/time-members/time-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-members/time-members/time-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/rpt-time-projects-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/rpt-time-projects.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/time-projects/time-projects.component.html
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/time-projects/time-projects.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/time-projects/time-projects.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/modules/rpt-time-reports/rpt-time-projects/time-projects/time-projects.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/pipes/with-count.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/pipes/with-count.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/reporting-api.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/reporting-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/reporting-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/reporting.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/reporting/reporting.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/project-schedule/project-schedule.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/project-schedule/project-schedule.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/project-schedule/project-schedule.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/project-schedule/project-schedule.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/add-member-allocation/add-member-allocation.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/add-member-allocation/add-member-allocation.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/add-member-allocation/add-member-allocation.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/add-member-allocation/add-member-allocation.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/context-menu/context-menu.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/context-menu/context-menu.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/context-menu/context-menu.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/context-menu/context-menu.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/end-date/end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/end-date/end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/end-date/end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/end-date/end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-indicator/member-indicator.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-indicator/member-indicator.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-indicator/member-indicator.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-indicator/member-indicator.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-task-add-container/member-task-add-container.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-task-add-container/member-task-add-container.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-task-add-container/member-task-add-container.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/member-task-add-container/member-task-add-container.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/phase/phase.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/phase/phase.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/phase/phase.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/phase/phase.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/priority/priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/priority/priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/priority/priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/priority/priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-indicator/project-indicator.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-indicator/project-indicator.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-indicator/project-indicator.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-indicator/project-indicator.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-member-tasks-drawer/project-member-tasks-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-member-tasks-drawer/project-member-tasks-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-member-tasks-drawer/project-member-tasks-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/project-member-tasks-drawer/project-member-tasks-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/start-date/start-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/start-date/start-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/start-date/start-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/start-date/start-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/status/status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/status/status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/status/status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/status/status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-input/task-add-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-input/task-add-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-input/task-add-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-input/task-add-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-row/task-add-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-row/task-add-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-row/task-add-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-add-row/task-add-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-header/task-list-header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-header/task-list-header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-header/task-list-header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-header/task-list-header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-row/task-list-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-row/task-list-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-row/task-list-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-list-row/task-list-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-name/task-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-name/task-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-name/task-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/task-name/task-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/tasks-context-menu/tasks-context-menu.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/tasks-context-menu/tasks-context-menu.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/tasks-context-menu/tasks-context-menu.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/components/tasks-context-menu/tasks-context-menu.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/projects-schedule.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/projects-schedule.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/projects-schedule.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/projects-schedule.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/service/project-schedule-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/service/schedule-member-tasks-hashmap-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/projects-schedule/service/schedule-member-tasks-service.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-v2/service/scheduler-common.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-view/schedule-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-view/schedule-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-view/schedule-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule-view/schedule-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/schedule.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/team-schedule/team-schedule.component.html
create mode 100644 worklenz-frontend/src/app/administrator/schedule/team-schedule/team-schedule.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/schedule/team-schedule/team-schedule.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/schedule/team-schedule/team-schedule.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories/categories.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories/categories.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories/categories.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/categories/categories/categories.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/change-password/change-password.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/change-password/change-password.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/change-password/change-password.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/change-password/change-password.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients/clients.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients/clients.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients/clients.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/clients/clients/clients.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles/job-titles.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles/job-titles.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles/job-titles.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/job-titles/job-titles/job-titles.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/labels/labels.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/labels/labels.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/labels/labels.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/labels/labels.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/language-and-region/language-and-region.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/language-and-region/language-and-region.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/language-and-region/language-and-region.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/language-and-region/language-and-region.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings/notification-settings.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings/notification-settings.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings/notification-settings.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/notification-settings/notification-settings/notification-settings.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/profile/profile.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/profile/profile.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/profile/profile.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/profile/profile.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/add-task-input/add-task-input.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/add-task-input/add-task-input.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/add-task-input/add-task-input.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/add-task-input/add-task-input.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/context-menu/context-menu.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/context-menu/context-menu.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/context-menu/context-menu.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/context-menu/context-menu.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/group-filter/group-filter.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/group-filter/group-filter.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/group-filter/group-filter.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/group-filter/group-filter.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/phase-settings-drawer/phase-settings-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/phase-settings-drawer/phase-settings-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/phase-settings-drawer/phase-settings-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/phase-settings-drawer/phase-settings-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-description/task-description.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-description/task-description.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-description/task-description.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-description/task-description.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-end-date/task-end-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-end-date/task-end-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-end-date/task-end-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-end-date/task-end-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-estimation/task-estimation.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-estimation/task-estimation.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-estimation/task-estimation.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-estimation/task-estimation.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-labels/task-labels.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-labels/task-labels.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-labels/task-labels.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-labels/task-labels.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-phase/task-phase.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-phase/task-phase.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-phase/task-phase.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-phase/task-phase.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-priority/task-priority.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-priority/task-priority.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-priority/task-priority.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-priority/task-priority.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-start-date/task-start-date.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-start-date/task-start-date.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-start-date/task-start-date.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-start-date/task-start-date.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-status/task-status.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-status/task-status.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-status/task-status.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/row/task-status/task-status.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/status-settings-drawer/status-settings-drawer.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/status-settings-drawer/status-settings-drawer.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/status-settings-drawer/status-settings-drawer.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/status-settings-drawer/status-settings-drawer.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-group-settings/task-list-group-settings.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-group-settings/task-list-group-settings.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-group-settings/task-list-group-settings.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-group-settings/task-list-group-settings.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-header/task-list-header.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-header/task-list-header.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-header/task-list-header.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/task-list-header/task-list-header.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/template-name/template-name.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/template-name/template-name.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/template-name/template-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/components/template-name/template-name.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/interfaces.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/pipes/ellipsis-tooltip-title.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/pipes/end-name-check.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/pipes/sub-tasks-arrow-color.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/pipes/sub-tasks-arrow-icon.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/pipes/truncate-if-long.pipe.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/project-template-edit-view.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/project-template-edit-view.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/project-template-edit-view.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/project-template-edit-view.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/services/pt-task-list-hash-map.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/services/pt-task-list.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/task-list-row/task-list-row.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/task-list-row/task-list-row.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/task-list-row/task-list-row.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-template-edit-view/task-list-row/task-list-row.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-templates/project-templates.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-templates/project-templates.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-templates/project-templates.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/project-templates/project-templates.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings-routing.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings.module.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings.service.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings.service.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings/settings.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings/settings.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings/settings.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/settings/settings.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/task-templates/task-templates.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/task-templates/task-templates.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/task-templates/task-templates.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/task-templates/task-templates.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/team-members/team-members.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/team-members/team-members.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/team-members/team-members.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/team-members/team-members.component.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/teams/teams.component.html
create mode 100644 worklenz-frontend/src/app/administrator/settings/teams/teams.component.scss
create mode 100644 worklenz-frontend/src/app/administrator/settings/teams/teams.component.spec.ts
create mode 100644 worklenz-frontend/src/app/administrator/settings/teams/teams.component.ts
create mode 100644 worklenz-frontend/src/app/app-routing.module.ts
create mode 100644 worklenz-frontend/src/app/app.component.html
create mode 100644 worklenz-frontend/src/app/app.component.scss
create mode 100644 worklenz-frontend/src/app/app.component.spec.ts
create mode 100644 worklenz-frontend/src/app/app.component.ts
create mode 100644 worklenz-frontend/src/app/app.module.ts
create mode 100644 worklenz-frontend/src/app/auth/auth-routing.module.ts
create mode 100644 worklenz-frontend/src/app/auth/auth.module.ts
create mode 100644 worklenz-frontend/src/app/auth/layout/layout.component.html
create mode 100644 worklenz-frontend/src/app/auth/layout/layout.component.scss
create mode 100644 worklenz-frontend/src/app/auth/layout/layout.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/layout/layout.component.ts
create mode 100644 worklenz-frontend/src/app/auth/login/login.component.html
create mode 100644 worklenz-frontend/src/app/auth/login/login.component.scss
create mode 100644 worklenz-frontend/src/app/auth/login/login.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/login/login.component.ts
create mode 100644 worklenz-frontend/src/app/auth/reset-password/reset-password.component.html
create mode 100644 worklenz-frontend/src/app/auth/reset-password/reset-password.component.scss
create mode 100644 worklenz-frontend/src/app/auth/reset-password/reset-password.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/reset-password/reset-password.component.ts
create mode 100644 worklenz-frontend/src/app/auth/signup/signup.component.html
create mode 100644 worklenz-frontend/src/app/auth/signup/signup.component.scss
create mode 100644 worklenz-frontend/src/app/auth/signup/signup.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/signup/signup.component.ts
create mode 100644 worklenz-frontend/src/app/auth/team-name/team-name.component.html
create mode 100644 worklenz-frontend/src/app/auth/team-name/team-name.component.scss
create mode 100644 worklenz-frontend/src/app/auth/team-name/team-name.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/team-name/team-name.component.ts
create mode 100644 worklenz-frontend/src/app/auth/verify-reset-email/verify-reset-email.component.html
create mode 100644 worklenz-frontend/src/app/auth/verify-reset-email/verify-reset-email.component.scss
create mode 100644 worklenz-frontend/src/app/auth/verify-reset-email/verify-reset-email.component.spec.ts
create mode 100644 worklenz-frontend/src/app/auth/verify-reset-email/verify-reset-email.component.ts
create mode 100644 worklenz-frontend/src/app/authenticate/authenticate.component.html
create mode 100644 worklenz-frontend/src/app/authenticate/authenticate.component.scss
create mode 100644 worklenz-frontend/src/app/authenticate/authenticate.component.spec.ts
create mode 100644 worklenz-frontend/src/app/authenticate/authenticate.component.ts
create mode 100644 worklenz-frontend/src/app/directives/infinite-scroll-trigger.directive.spec.ts
create mode 100644 worklenz-frontend/src/app/directives/infinite-scroll-trigger.directive.ts
create mode 100644 worklenz-frontend/src/app/directives/lazy-for.directive.spec.ts
create mode 100644 worklenz-frontend/src/app/directives/lazy-for.directive.ts
create mode 100644 worklenz-frontend/src/app/errors/not-authorized/not-authorized.component.html
create mode 100644 worklenz-frontend/src/app/errors/not-authorized/not-authorized.component.scss
create mode 100644 worklenz-frontend/src/app/errors/not-authorized/not-authorized.component.spec.ts
create mode 100644 worklenz-frontend/src/app/errors/not-authorized/not-authorized.component.ts
create mode 100644 worklenz-frontend/src/app/errors/not-found/not-found.component.html
create mode 100644 worklenz-frontend/src/app/errors/not-found/not-found.component.scss
create mode 100644 worklenz-frontend/src/app/errors/not-found/not-found.component.spec.ts
create mode 100644 worklenz-frontend/src/app/errors/not-found/not-found.component.ts
create mode 100644 worklenz-frontend/src/app/guards/auth.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/auth.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/login-check.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/login-check.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/non-google-account.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/non-google-account.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/team-member.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/team-member.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/team-name.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/team-name.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/team-owner-or-admin-guard.service.ts
create mode 100644 worklenz-frontend/src/app/guards/team-owner-or-admin.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/team-owner.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/team-owner.guard.ts
create mode 100644 worklenz-frontend/src/app/guards/team-url.guard.spec.ts
create mode 100644 worklenz-frontend/src/app/guards/team-url.guard.ts
create mode 100644 worklenz-frontend/src/app/interceptors/http.interceptor.spec.ts
create mode 100644 worklenz-frontend/src/app/interceptors/http.interceptor.ts
create mode 100644 worklenz-frontend/src/app/interfaces/account-center.ts
create mode 100644 worklenz-frontend/src/app/interfaces/allocation-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/activity-log.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/activity-logs-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/authorize-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-assign-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-delete-tasks-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-archive-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-delete-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-delete-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-labels-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-phase-change-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-priority-change-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/bulk-tasks-status-change-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/client-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/clients-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/gantt.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/inline-member.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/job-titles-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/local-session.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/my-dashboard-all-tasks-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/organization-team-get-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/organization-users-get-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-comment-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-insights.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-members-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-tasks-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-template.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/project-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/projects-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/projects-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/reporting.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/reset-password-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/server-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-attachment-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-attachment.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-comment-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-comments-create-request.ts.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-list-config.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-log-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-phase.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-priorities-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-status-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-status-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-status-update-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-template-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/task-templates-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-activate-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-invitation-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-member-create-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-members-get-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-members-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/team-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/user-login-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/user-login-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/user-sign-up-request.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/user-sign-up-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/api-models/verify-reset-email.ts
create mode 100644 worklenz-frontend/src/app/interfaces/avatar-attachment.ts
create mode 100644 worklenz-frontend/src/app/interfaces/client.ts
create mode 100644 worklenz-frontend/src/app/interfaces/gantt-chart.ts
create mode 100644 worklenz-frontend/src/app/interfaces/invitation-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/job-title.ts
create mode 100644 worklenz-frontend/src/app/interfaces/my-tasks.ts
create mode 100644 worklenz-frontend/src/app/interfaces/nav-item-type.ts
create mode 100644 worklenz-frontend/src/app/interfaces/nav-item.ts
create mode 100644 worklenz-frontend/src/app/interfaces/notification-settings.ts
create mode 100644 worklenz-frontend/src/app/interfaces/notification.ts
create mode 100644 worklenz-frontend/src/app/interfaces/pagination-component.ts
create mode 100644 worklenz-frontend/src/app/interfaces/password-strength-check.ts
create mode 100644 worklenz-frontend/src/app/interfaces/password-validity-result.ts
create mode 100644 worklenz-frontend/src/app/interfaces/personal-overview.ts
create mode 100644 worklenz-frontend/src/app/interfaces/profile-settings.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-access-level.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-category.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-comments.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-folder.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-health.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-manager.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-member.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-status.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-template.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project-wise-resources-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/project.ts
create mode 100644 worklenz-frontend/src/app/interfaces/projects-filter-config.ts
create mode 100644 worklenz-frontend/src/app/interfaces/reporting-allocation-settings.ts
create mode 100644 worklenz-frontend/src/app/interfaces/reporting.ts
create mode 100644 worklenz-frontend/src/app/interfaces/roadmap.ts
create mode 100644 worklenz-frontend/src/app/interfaces/role.ts
create mode 100644 worklenz-frontend/src/app/interfaces/schedular.ts
create mode 100644 worklenz-frontend/src/app/interfaces/selectable-category.ts
create mode 100644 worklenz-frontend/src/app/interfaces/selectable-project.ts
create mode 100644 worklenz-frontend/src/app/interfaces/selectable-team.ts
create mode 100644 worklenz-frontend/src/app/interfaces/settings-navigation-item.ts
create mode 100644 worklenz-frontend/src/app/interfaces/sub-task.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-assignee-update-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-assignee.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-comment.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-form-view-model.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-label.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-list-column.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-list-estimation-change-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-list-status-change-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-phase-change-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-priority.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-status-category.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task-status.ts
create mode 100644 worklenz-frontend/src/app/interfaces/task.ts
create mode 100644 worklenz-frontend/src/app/interfaces/team-member.ts
create mode 100644 worklenz-frontend/src/app/interfaces/team.ts
create mode 100644 worklenz-frontend/src/app/interfaces/timer-start-event-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/timer-stop-event-response.ts
create mode 100644 worklenz-frontend/src/app/interfaces/timezone.ts
create mode 100644 worklenz-frontend/src/app/interfaces/todo-list-item.ts
create mode 100644 worklenz-frontend/src/app/interfaces/user.ts
create mode 100644 worklenz-frontend/src/app/interfaces/worklenz-notification.ts
create mode 100644 worklenz-frontend/src/app/interfaces/workload.ts
create mode 100644 worklenz-frontend/src/app/pipes/bind-na.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/bind-na.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/date-formatter.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/ellipsis.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/ellipsis.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/first-char-upper.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/first-char-upper.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/from-now.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/from-now.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/min-date-validator.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/safe-string.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/safe-string.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/search-by-name.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/search-by-name.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/task-comment-mention.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/task-comment-mention.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/to-now.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/to-now.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/with-alpha.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/with-alpha.pipe.ts
create mode 100644 worklenz-frontend/src/app/pipes/wl-safe-array.pipe.spec.ts
create mode 100644 worklenz-frontend/src/app/pipes/wl-safe-array.pipe.ts
create mode 100644 worklenz-frontend/src/app/services/api/access-controls-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/account-center-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/activity-logs.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/api/activity-logs.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/api-service-base.ts
create mode 100644 worklenz-frontend/src/app/services/api/attachments-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/auth-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/clients-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/gantt-api.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/api/gantt-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/home-page-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/job-titles-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/logs-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/notifications-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/personal-overview.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/profile-settings-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-categories-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-categories.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-comments-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-folders-api.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-folders-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-healths-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-insights.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-managers-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-members-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-roadmap-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-statuses-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-template-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/project-workload-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/projects-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/pt-labels-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/pt-priorities-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/pt-statuses-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/pt-task-phases-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/pt-tasks-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/reporting-api-v0.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/reporting-export-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/resource-allocation.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/schedule-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/shared-projects-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/sub-tasks-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-comments-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-labels-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-phases-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-priorities.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-statuses-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/task-templates.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/tasks-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/tasks-log-time.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/team-members-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/teams-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/test-gannt-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/timezones-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/todo-list-api.service.ts
create mode 100644 worklenz-frontend/src/app/services/api/users.service.ts
create mode 100644 worklenz-frontend/src/app/services/app.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/app.service.ts
create mode 100644 worklenz-frontend/src/app/services/auth.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/auth.service.ts
create mode 100644 worklenz-frontend/src/app/services/menu-service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/menu.service.ts
create mode 100644 worklenz-frontend/src/app/services/notification-settings.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/notification-settings.service.ts
create mode 100644 worklenz-frontend/src/app/services/project-form-service.service.ts
create mode 100644 worklenz-frontend/src/app/services/project-phases.service.ts
create mode 100644 worklenz-frontend/src/app/services/project-template.service.ts
create mode 100644 worklenz-frontend/src/app/services/project-updates.service.ts
create mode 100644 worklenz-frontend/src/app/services/socket.service.ts
create mode 100644 worklenz-frontend/src/app/services/utils.service.spec.ts
create mode 100644 worklenz-frontend/src/app/services/utils.service.ts
create mode 100644 worklenz-frontend/src/app/services/worklenz.analytics.ts
create mode 100644 worklenz-frontend/src/app/session-expired/session-expired.component.html
create mode 100644 worklenz-frontend/src/app/session-expired/session-expired.component.scss
create mode 100644 worklenz-frontend/src/app/session-expired/session-expired.component.spec.ts
create mode 100644 worklenz-frontend/src/app/session-expired/session-expired.component.ts
create mode 100644 worklenz-frontend/src/app/shared/constants.ts
create mode 100644 worklenz-frontend/src/app/shared/events.ts
create mode 100644 worklenz-frontend/src/app/shared/session-helper.ts
create mode 100644 worklenz-frontend/src/app/shared/socket-events.ts
create mode 100644 worklenz-frontend/src/app/shared/utils.ts
create mode 100644 worklenz-frontend/src/app/shared/worklenz-analytics-events.ts
create mode 100644 worklenz-frontend/src/assets/.gitkeep
create mode 100644 worklenz-frontend/src/assets/css/prebuilt-editor.css
create mode 100644 worklenz-frontend/src/assets/icons/icon-128x128.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-144x144.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-152x152.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-192x192.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-384x384.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-512x512.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-72x72.png
create mode 100644 worklenz-frontend/src/assets/icons/icon-96x96.png
create mode 100644 worklenz-frontend/src/assets/images/404.svg
create mode 100644 worklenz-frontend/src/assets/images/block-user.png
create mode 100644 worklenz-frontend/src/assets/images/chevron-down-solid.svg
create mode 100644 worklenz-frontend/src/assets/images/clipboard.png
create mode 100644 worklenz-frontend/src/assets/images/clock-green.png
create mode 100644 worklenz-frontend/src/assets/images/clock-red.png
create mode 100644 worklenz-frontend/src/assets/images/clock-yellow.png
create mode 100644 worklenz-frontend/src/assets/images/confetti (1).png
create mode 100644 worklenz-frontend/src/assets/images/confetti.png
create mode 100644 worklenz-frontend/src/assets/images/empty-box.png
create mode 100644 worklenz-frontend/src/assets/images/empty-box.webp
create mode 100644 worklenz-frontend/src/assets/images/files/ai.png
create mode 100644 worklenz-frontend/src/assets/images/files/avi.png
create mode 100644 worklenz-frontend/src/assets/images/files/css.png
create mode 100644 worklenz-frontend/src/assets/images/files/csv.png
create mode 100644 worklenz-frontend/src/assets/images/files/doc.png
create mode 100644 worklenz-frontend/src/assets/images/files/exe.png
create mode 100644 worklenz-frontend/src/assets/images/files/html.png
create mode 100644 worklenz-frontend/src/assets/images/files/jpg.png
create mode 100644 worklenz-frontend/src/assets/images/files/js.png
create mode 100644 worklenz-frontend/src/assets/images/files/json.png
create mode 100644 worklenz-frontend/src/assets/images/files/mp3.png
create mode 100644 worklenz-frontend/src/assets/images/files/mp4.png
create mode 100644 worklenz-frontend/src/assets/images/files/pdf.png
create mode 100644 worklenz-frontend/src/assets/images/files/png.png
create mode 100644 worklenz-frontend/src/assets/images/files/ppt.png
create mode 100644 worklenz-frontend/src/assets/images/files/psd.png
create mode 100644 worklenz-frontend/src/assets/images/files/search.png
create mode 100644 worklenz-frontend/src/assets/images/files/svg.png
create mode 100644 worklenz-frontend/src/assets/images/files/txt.png
create mode 100644 worklenz-frontend/src/assets/images/files/xls.png
create mode 100644 worklenz-frontend/src/assets/images/files/xml.png
create mode 100644 worklenz-frontend/src/assets/images/files/zip.png
create mode 100644 worklenz-frontend/src/assets/images/folder.svg
create mode 100644 worklenz-frontend/src/assets/images/google-icon.png
create mode 100644 worklenz-frontend/src/assets/images/google_sign_in.png
create mode 100644 worklenz-frontend/src/assets/images/graph-report.png
create mode 100644 worklenz-frontend/src/assets/images/group.png
create mode 100644 worklenz-frontend/src/assets/images/group_1.png
create mode 100644 worklenz-frontend/src/assets/images/insights-calendar.png
create mode 100644 worklenz-frontend/src/assets/images/insights-check.png
create mode 100644 worklenz-frontend/src/assets/images/logo-lg.png
create mode 100644 worklenz-frontend/src/assets/images/logo-sm.png
create mode 100644 worklenz-frontend/src/assets/images/logo.png
create mode 100644 worklenz-frontend/src/assets/images/magnifying-glass-solid.svg
create mode 100644 worklenz-frontend/src/assets/images/noimage.png
create mode 100644 worklenz-frontend/src/assets/images/open-icon.svg
create mode 100644 worklenz-frontend/src/assets/images/progress.png
create mode 100644 worklenz-frontend/src/assets/images/project-management.png
create mode 100644 worklenz-frontend/src/assets/images/team-members.svg
create mode 100644 worklenz-frontend/src/assets/images/to-do-list.png
create mode 100644 worklenz-frontend/src/assets/images/user.png
create mode 100644 worklenz-frontend/src/assets/images/warning.png
create mode 100644 worklenz-frontend/src/environments/environment.prod.ts
create mode 100644 worklenz-frontend/src/environments/environment.ts
create mode 100644 worklenz-frontend/src/favicon.ico
create mode 100644 worklenz-frontend/src/index.html
create mode 100644 worklenz-frontend/src/main.ts
create mode 100644 worklenz-frontend/src/manifest.webmanifest
create mode 100644 worklenz-frontend/src/res/js/jquery-3.6.1.min.js
create mode 100644 worklenz-frontend/src/res/js/jquery.mentiony.js
create mode 100644 worklenz-frontend/src/res/js/smooth-scroll.js
create mode 100644 worklenz-frontend/src/res/scss/antd-variables.scss
create mode 100644 worklenz-frontend/src/res/scss/antd.scss
create mode 100644 worklenz-frontend/src/res/scss/gantt.scss
create mode 100644 worklenz-frontend/src/scss/editable-table.scss
create mode 100644 worklenz-frontend/src/scss/scrollbar.scss
create mode 100644 worklenz-frontend/src/scss/task-view.scss
create mode 100644 worklenz-frontend/src/styles.scss
create mode 100644 worklenz-frontend/src/test.ts
create mode 100644 worklenz-frontend/src/theme.less
create mode 100644 worklenz-frontend/src/typings.d.ts
create mode 100644 worklenz-frontend/tsconfig.app.json
create mode 100644 worklenz-frontend/tsconfig.json
create mode 100644 worklenz-frontend/tsconfig.spec.json
diff --git a/SETUP_THE_PROJECT.md b/SETUP_THE_PROJECT.md
index dec6e93c..57df4266 100644
--- a/SETUP_THE_PROJECT.md
+++ b/SETUP_THE_PROJECT.md
@@ -56,7 +56,12 @@ Getting started with development is a breeze! Follow these steps and you'll be c
- Create a copy of the `.env.template` file and name it `.env`.
- Update the required fields in `.env` with the specific information.
-4. **Install Dependencies:**
+4. **Restore Database**
+ - Create a new database named `worklenz_db` on your local PostgreSQL server.
+ - Update the `DATABASE_NAME` and `PASSWORD` in the `database/6_user_permission.sql` with your DB credentials.
+ - Open a query console and execute the queries from the .sql files in the `database` directories, following the provided order.
+
+5. **Install Dependencies:**
```bash
npm install
@@ -64,7 +69,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
This command installs all the necessary libraries required to run the project.
-5. **Run the Development Server:**
+6. **Run the Development Server:**
**a. Start the TypeScript compiler:**
@@ -86,7 +91,7 @@ Getting started with development is a breeze! Follow these steps and you'll be c
This starts the development server allowing you to work on the project.
-6. **Run the Production Server:**
+7. **Run the Production Server:**
**a. Compile TypeScript to JavaScript:**
diff --git a/worklenz-backend/.dockerignore b/worklenz-backend/.dockerignore
new file mode 100644
index 00000000..263e9ac4
--- /dev/null
+++ b/worklenz-backend/.dockerignore
@@ -0,0 +1,5 @@
+node_modules
+npm-debug.log
+build
+.scannerwork
+coverage
diff --git a/worklenz-backend/.editorconfig b/worklenz-backend/.editorconfig
new file mode 100644
index 00000000..5f4cb0f0
--- /dev/null
+++ b/worklenz-backend/.editorconfig
@@ -0,0 +1,19 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+
+[*.pug,]
+indent_style = tab
+indent_size = 4
+
+[*.sql]
+indent_style = space
+indent_size = 4
\ No newline at end of file
diff --git a/worklenz-backend/.env.template b/worklenz-backend/.env.template
new file mode 100644
index 00000000..91a96c9c
--- /dev/null
+++ b/worklenz-backend/.env.template
@@ -0,0 +1,55 @@
+# Server
+NODE_ENV=development
+PORT=3000
+SESSION_NAME=worklenz.sid
+SESSION_SECRET="%7Z3Vn$d2sJdhbwXdZp2GhhDH6f5Z%_43bX#B-@fA=ntXVDsq$b=AuUm_NXZ7hTX?3EKpHwdAsnbUm-Sv!&bcX=xy2pfkwnTfeW$qhVu4_L&N-+s6gUY2V$%E=fDU^a?8ghjav?b!aGPfvAb7yKzSRR#4YsfgrHfMARQBmgF*_AzsHV@2=Jgz?wtERUHLDPh4R6t3VH7UydrgtQG24SaD?q_qFM_PX*_QDYs!8*An=aEpbbMJMVFj5t43_wWM$mM"
+COOKIE_SECRET="CeD=C+_2LSSFF_qJ@cCz_$=KQv=ZrxU?DDL9$9%Yrd^yeeZ&h#QCSvX@u9^M!y%fnw^SU$-MQetU!eKLWR@n_pafJSU%*?nvr&qKgnUsj?5+Jnw$rFuPGWej-L&Cznk+rcKZ#8%Wcr$y%KAzCp597Z2Tnt?gkx=xsc%RNjcfkYeA=94JnLJxKur8p*HJ4?Q#5U%@BMhR4n67a-rZJEvnkFgxVcvaLdmEjXFe#26UkJV799MPP5wU7-&fpx4Vfkf="
+
+# CORS
+SOCKET_IO_CORS=http://localhost:4200
+SERVER_CORS=*
+
+# Database
+DB_USER=DATABASE_USER_NAME_HERE
+DB_PASSWORD=DATABASE_PASSWORD_HERE
+DB_NAME=DATABASE_NAME_HERE
+DB_HOST=DATABASE_HOST_HERE # localhost
+DB_PORT=DATABASE_PORT_HERE
+DB_MAX_CLIENTS=50
+
+# Google Login
+GOOGLE_CLIENT_ID="GOOGLE_CLIENT_ID_HERE"
+GOOGLE_CLIENT_SECRET="GOOGLE_CLIENT_SECRET_HERE"
+GOOGLE_CALLBACK_URL="http://localhost:3000/secure/google/verify"
+LOGIN_FAILURE_REDIRECT="/"
+LOGIN_SUCCESS_REDIRECT="http://localhost:4200/auth/authenticate"
+
+# CLI
+ANGULAR_DIST_DIR="/path/worklenz_frontend/dist/worklenz"
+ANGULAR_SRC_DIR="/path/worklenz_frontend"
+BACKEND_PUBLIC_DIR="/path/worklenz_backend/src/public"
+BACKEND_VIEWS_DIR="/path/worklenz_backend/src/views/admin"
+COMMIT_BUILD_IMMEDIATELY=true
+
+# HOST
+HOSTNAME=localhost:4200
+
+# SLACK
+SLACK_WEBHOOK=SLACK_WEBHOOK_HERE
+USE_PG_NATIVE=true
+
+# JWT SECRET
+JWT_SECRET=JWT_SECRET_CODE_HERE
+
+# AWS
+AWS_REGION="us-west-2"
+AWS_ACCESS_KEY_ID="AWS_ACCESS_KEY_ID_HERE" # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+AWS_SECRET_ACCESS_KEY="AWS_SECRET_ACCESS_KEY_HERE" # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+# S3 Credentials
+REGION="us-west-2"
+BUCKET="BUCKET_NAME_HERE"
+S3_URL="S3_URL_HERE"
+S3_ACCESS_KEY_ID="S3_ACCESS_KEY_ID_HERE"
+S3_SECRET_ACCESS_KEY="S3_SECRET_ACCESS_KEY_HERE"
+
diff --git a/worklenz-backend/.eslintrc.json b/worklenz-backend/.eslintrc.json
new file mode 100644
index 00000000..106805c2
--- /dev/null
+++ b/worklenz-backend/.eslintrc.json
@@ -0,0 +1,109 @@
+{
+ "root": true,
+ "parser": "@typescript-eslint/parser",
+ "plugins": [
+ "@typescript-eslint"
+ ],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:security/recommended"
+ ],
+ "parserOptions": {
+ "ecmaVersion": 2017,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "spread": true,
+ "experimentalObjectRestSpread": true
+ }
+ },
+ "globals": {
+ "window": true,
+ "document": true,
+ "angular": true
+ },
+ "rules": {
+ "constructor-super": 2,
+ "no-class-assign": 2,
+ "no-cond-assign": 2,
+ "no-console": 1,
+ "no-const-assign": 2,
+ "no-constant-condition": 2,
+ "no-control-regex": 2,
+ "no-debugger": 2,
+ "no-delete-var": 2,
+ "no-dupe-args": 2,
+ "no-dupe-class-members": 2,
+ "no-dupe-keys": 2,
+ "no-duplicate-case": 2,
+ "no-empty-character-class": 2,
+ "no-empty-pattern": 2,
+ "no-empty": 2,
+ "no-ex-assign": 2,
+ "no-extra-boolean-cast": 2,
+ "no-extra-semi": 2,
+ "no-fallthrough": 2,
+ "no-func-assign": 2,
+ "no-global-assign": 2,
+ "no-inner-declarations": 2,
+ "no-invalid-regexp": 2,
+ "no-irregular-whitespace": 2,
+ "no-mixed-spaces-and-tabs": 2,
+ "no-new-symbol": 2,
+ "no-obj-calls": 2,
+ "no-octal": 2,
+ "no-redeclare": 2,
+ "no-regex-spaces": 2,
+ "no-self-assign": 2,
+ "no-sparse-arrays": 2,
+ "no-this-before-super": 2,
+ "no-undef": 2,
+ "no-unexpected-multiline": 2,
+ "no-unreachable": 2,
+ "no-unsafe-finally": 2,
+ "no-unsafe-negation": 2,
+ "no-unused-labels": 2,
+ "no-unused-vars": 1,
+ "no-useless-escape": 1,
+ "require-yield": 2,
+ "use-isnan": 2,
+ "valid-typeof": 2,
+ "no-var": 2,
+ "no-eval": 2,
+ "quotes": [
+ 2,
+ "double",
+ {
+ "allowTemplateLiterals": true
+ }
+ ],
+ "capitalized-comments": 0,
+ "no-use-before-define": 2,
+ "no-else-return": 2,
+ "no-invalid-this": 2,
+ "object-shorthand": 2,
+ "quote-props": 0,
+ "no-array-constructor": 2,
+ "no-new-func": 2,
+ "no-new-object": 2,
+ "prefer-destructuring": 1,
+ "prefer-template": 2,
+ "no-param-reassign": 2,
+ "prefer-spread": 2,
+ "arrow-spacing": 2,
+ "keyword-spacing": 2,
+ "space-infix-ops": 2,
+ "space-before-blocks": 2,
+ "object-curly-spacing": 0,
+ "semi": 2,
+ "no-underscore-dangle": 2,
+ "prefer-arrow-callback": 2,
+ "prefer-const": 2
+ },
+ "env": {
+ "node": true,
+ "jest": true,
+ "es6": true
+ }
+}
diff --git a/worklenz-backend/.gitignore b/worklenz-backend/.gitignore
new file mode 100644
index 00000000..d9d5a80a
--- /dev/null
+++ b/worklenz-backend/.gitignore
@@ -0,0 +1,65 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+.DS_Store
+config.json
+.idea
+build
+.vscode
+*.code-workspace
diff --git a/worklenz-backend/.gitmodules b/worklenz-backend/.gitmodules
new file mode 100644
index 00000000..c237f94d
--- /dev/null
+++ b/worklenz-backend/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "worklenz-email-templates"]
+ path = worklenz-email-templates
+ url = "URL_HERE"
diff --git a/worklenz-backend/.npmrc b/worklenz-backend/.npmrc
new file mode 100644
index 00000000..e49cca44
--- /dev/null
+++ b/worklenz-backend/.npmrc
@@ -0,0 +1,2 @@
+engine-strict=true
+fund=false # Don't print the trailing funding message
diff --git a/worklenz-backend/Dockerfile b/worklenz-backend/Dockerfile
new file mode 100644
index 00000000..c0ae5385
--- /dev/null
+++ b/worklenz-backend/Dockerfile
@@ -0,0 +1,26 @@
+# Use the official Node.js 18 image as a base
+FROM node:18
+
+# Create and set the working directory
+WORKDIR /usr/src/app
+
+# Install global dependencies
+RUN npm install -g ts-node typescript grunt grunt-cli
+
+# Copy package.json and package-lock.json (if available)
+COPY package*.json ./
+
+# Install app dependencies
+RUN npm ci
+
+# Copy the rest of the application code
+COPY . .
+
+# Run the build script to compile TypeScript to JavaScript
+RUN npm run build
+
+# Expose the port the app runs on
+EXPOSE 3000
+
+# Start the application
+CMD ["npm", "start"]
diff --git a/worklenz-backend/Gruntfile.js b/worklenz-backend/Gruntfile.js
new file mode 100644
index 00000000..b621cbc0
--- /dev/null
+++ b/worklenz-backend/Gruntfile.js
@@ -0,0 +1,131 @@
+module.exports = function (grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ pkg: grunt.file.readJSON("package.json"),
+ clean: {
+ dist: "build"
+ },
+ compress: require("./grunt/grunt-compress"),
+ copy: {
+ main: {
+ files: [
+ {expand: true, cwd: "src", src: ["public/**"], dest: "build"},
+ {expand: true, cwd: "src", src: ["views/**"], dest: "build"},
+ {expand: true, cwd: "landing-page-assets", src: ["**"], dest: "build/public/assets"},
+ {expand: true, cwd: "src", src: ["shared/sample-data.json"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "src", src: ["shared/templates/**"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "src", src: ["shared/postgresql-error-codes.json"], dest: "build", filter: "isFile"},
+ ]
+ },
+ packages: {
+ files: [
+ {expand: true, cwd: "", src: [".env"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: [".gitignore"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: ["release"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: ["jest.config.js"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: ["package.json"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: ["package-lock.json"], dest: "build", filter: "isFile"},
+ {expand: true, cwd: "", src: ["common_modules/**"], dest: "build"}
+ ]
+ }
+ },
+ sync: {
+ main: {
+ files: [
+ {cwd: "src", src: ["views/**", "public/**"], dest: "build/"}, // makes all src relative to cwd
+ ],
+ verbose: true,
+ failOnError: true,
+ compareUsing: "md5"
+ }
+ },
+ uglify: {
+ all: {
+ files: [{
+ expand: true,
+ cwd: "build",
+ src: "**/*.js",
+ dest: "build"
+ }]
+ },
+ controllers: {
+ files: [{
+ expand: true,
+ cwd: "build",
+ src: "controllers/*.js",
+ dest: "build"
+ }]
+ },
+ routes: {
+ files: [{
+ expand: true,
+ cwd: "build",
+ src: "routes/**/*.js",
+ dest: "build"
+ }]
+ },
+ assets: {
+ files: [{
+ expand: true,
+ cwd: "build",
+ src: "public/assets/**/*.js",
+ dest: "build"
+ }]
+ }
+ },
+ shell: {
+ tsc: {
+ command: "tsc --build tsconfig.prod.json"
+ },
+ esbuild: {
+ // command: "esbuild `find src -type f -name '*.ts'` --platform=node --minify=false --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=build"
+ command: "node esbuild && node cli/esbuild-patch"
+ },
+ tsc_dev: {
+ command: "tsc --build tsconfig.json"
+ },
+ swagger: {
+ command: "node ./cli/swagger"
+ },
+ inline_queries: {
+ command: "node ./cli/inline-queries"
+ }
+ },
+ watch: {
+ scripts: {
+ files: ["src/**/*.ts"],
+ tasks: ["shell:tsc_dev"],
+ options: {
+ debounceDelay: 250,
+ spawn: false,
+ }
+ },
+ other: {
+ files: ["src/**/*.pug", "landing-page-assets/**"],
+ tasks: ["sync"]
+ }
+ }
+ });
+
+ grunt.registerTask("clean", ["clean"]);
+ grunt.registerTask("copy", ["copy:main"]);
+ grunt.registerTask("swagger", ["shell:swagger"]);
+ grunt.registerTask("build:tsc", ["shell:tsc"]);
+ grunt.registerTask("build", ["clean", "shell:tsc", "copy:main", "compress"]);
+ grunt.registerTask("build:es", ["clean", "shell:esbuild", "copy:main", "uglify:assets", "compress"]);
+ grunt.registerTask("build:strict", ["clean", "shell:tsc", "copy:packages", "uglify:all", "copy:main", "compress"]);
+ grunt.registerTask("dev", ["clean", "copy:main", "shell:tsc_dev", "shell:inline_queries", "watch"]);
+
+ // Load the plugin that provides the "uglify" task.
+ grunt.loadNpmTasks("grunt-contrib-watch");
+ grunt.loadNpmTasks("grunt-contrib-clean");
+ grunt.loadNpmTasks("grunt-contrib-copy");
+ grunt.loadNpmTasks("grunt-contrib-uglify");
+ grunt.loadNpmTasks("grunt-contrib-compress");
+ grunt.loadNpmTasks("grunt-shell");
+ grunt.loadNpmTasks("grunt-sync");
+
+ // Default task(s).
+ grunt.registerTask("default", []);
+};
diff --git a/worklenz-backend/README.md b/worklenz-backend/README.md
new file mode 100644
index 00000000..25590300
--- /dev/null
+++ b/worklenz-backend/README.md
@@ -0,0 +1,81 @@
+# Worklenz Backend
+
+1. **Open your IDE:**
+
+ Open the project directory in your preferred code editor or IDE like Visual Studio Code.
+
+2. **Configure Environment Variables:**
+
+ - Create a copy of the `.env.template` file and name it `.env`.
+ - Update the required fields in `.env` with the specific information.
+
+3. **Restore Database**
+ - Create a new database named `worklenz_db` on your local PostgreSQL server.
+ - Update the `DATABASE_NAME` and `PASSWORD` in the `database/6_user_permission.sql` with your DB credentials.
+ - Open a query console and execute the queries from the .sql files in the `database` directories, following the provided order.
+
+4. **Install Dependencies:**
+
+ ```bash
+ npm install
+ ```
+
+ This command installs all the necessary libraries required to run the project.
+
+5. **Run the Development Server:**
+
+ **a. Start the TypeScript compiler:**
+
+ Open a new terminal window and run the following command:
+
+ ```bash
+ grunt dev
+ ```
+
+ This starts the `grunt` task runner, which compiles TypeScript code into JavaScript.
+
+ **b. Start the development server:**
+
+ Open another separate terminal window and run the following command:
+
+ ```bash
+ npm start
+ ```
+
+ This starts the development server allowing you to work on the project.
+
+6. **Run the Production Server:**
+
+ **a. Compile TypeScript to JavaScript:**
+
+ Open a new terminal window and run the following command:
+
+ ```bash
+ grunt build
+ ```
+
+ This starts the `grunt` task runner, which compiles TypeScript code into JavaScript for production use.
+
+ **b. Start the production server:**
+
+ Once the compilation is complete, run the following command in the same terminal window:
+
+ ```bash
+ npm start
+ ```
+
+ This starts the production server for your application.
+
+### CLI
+
+- Create controller: `$ node new controller Test`
+- Create angular release: `$ node new release`
+
+### Developement Rules
+
+- Controllers should only generate/create using the CLI (`node new controller Projects`)
+- Validations should only be done using a middleware placed under src/validators/ and used inside the routers (E.g., api-router.ts)
+- Validators should only generate/create using the CLI (`node new vaidator projects-params`)
+
+## Pull submodules
+- git submodule update --init --recursive
diff --git a/worklenz-backend/appspec.yml b/worklenz-backend/appspec.yml
new file mode 100644
index 00000000..b266bc92
--- /dev/null
+++ b/worklenz-backend/appspec.yml
@@ -0,0 +1,18 @@
+version: 0.0
+os: linux
+files:
+ - source: /
+ destination: "DESTINATION_HERE"
+permissions:
+ - object: "OBJECT_PATH_HERE"
+ pattern: "*.sh"
+ # owner: root
+ # group: root
+ mode: 775
+ type:
+ - file
+hooks:
+ AfterInstall:
+ - location: build.sh
+ timeout: 3600
+ runas: root
diff --git a/worklenz-backend/babel.config.js b/worklenz-backend/babel.config.js
new file mode 100644
index 00000000..dd242dc9
--- /dev/null
+++ b/worklenz-backend/babel.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ presets: [
+ ["@babel/preset-env", { targets: { node: "current" } }],
+ "@babel/preset-typescript",
+ ],
+};
diff --git a/worklenz-backend/build.sh b/worklenz-backend/build.sh
new file mode 100644
index 00000000..42022bd6
--- /dev/null
+++ b/worklenz-backend/build.sh
@@ -0,0 +1,3 @@
+cd "PATH_HERE"
+npm ci
+npm run build
diff --git a/worklenz-backend/cli/esbuild-patch b/worklenz-backend/cli/esbuild-patch
new file mode 100644
index 00000000..7b28f7d1
--- /dev/null
+++ b/worklenz-backend/cli/esbuild-patch
@@ -0,0 +1,16 @@
+const fs = require("fs");
+const path = require("path");
+
+const wwwFile = fs.readFileSync(path.join(__dirname, "../build/bin/www.js"), "utf8");
+
+const bluebird = /(global\.Promise = require\("bluebird"\);)/g;
+const dotenvImport = /(var import_dotenv = __toESM\(require\("dotenv"\)\);)/g;
+const dotenvUse = /(import_dotenv\.default\.config\(\);)/g;
+
+const out = wwwFile
+ .replace(bluebird, "")
+ .replace(dotenvUse, "")
+ .replace(dotenvImport, `var import_dotenv = __toESM(require("dotenv"));import_dotenv.default.config();`)
+ .replace(dotenvUse, `import_dotenv.default.config();global.Promise = require("bluebird");`);
+
+fs.writeFileSync(path.join(__dirname, "../build/bin/www.js"), out, "utf8");
diff --git a/worklenz-backend/cli/generate-controller b/worklenz-backend/cli/generate-controller
new file mode 100644
index 00000000..344f0486
--- /dev/null
+++ b/worklenz-backend/cli/generate-controller
@@ -0,0 +1,106 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+const path = require("path");
+
+const arg = process.argv[2];
+
+function camelize(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => {
+ return index === 0 ? word.toLowerCase() : word.toUpperCase();
+ }).replace(/[\W]/g, "");
+}
+
+function toPascalCase(string) {
+ return `${string}`
+ .toLowerCase()
+ .replace(new RegExp(/[-_]+/, "g"), "")
+ .replace(new RegExp(/[^\w\s]/, "g"), "")
+ .replace(
+ new RegExp(/\s+(.)(\w*)/, "g"),
+ ($1, $2, $3) => `${$2.toUpperCase() + $3}`
+ )
+ .replace(new RegExp(/\w/), s => s.toUpperCase());
+}
+
+const name = arg.trim();
+const fileName = name.toLowerCase();
+const varName = camelize(name);
+const Controller = `${toPascalCase(name)}Controller`;
+
+const content = `
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class ${Controller} extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = \`\`;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = \`\`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = \`\`;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = \`\`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = \`\`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
+`;
+
+const fullName = `${name}-controller`;
+
+const apis = `
+import express from "express";
+
+import ${Controller} from "../../controllers/${fullName}";
+
+const ${varName}ApiRouter = express.Router();
+
+${varName}ApiRouter.post("/", ${Controller}.create);
+${varName}ApiRouter.get("/", ${Controller}.get);
+${varName}ApiRouter.get("/:id", ${Controller}.getById);
+${varName}ApiRouter.put("/:id", ${Controller}.update);
+${varName}ApiRouter.delete("/:id", ${Controller}.deleteById);
+
+export default ${varName}ApiRouter;
+`;
+
+fs.writeFileSync(path.join(__dirname, "../src/controllers", `${fullName}.ts`), content.trim(), "utf8");
+fs.writeFileSync(path.join(__dirname, "../src/routes/apis/", `${fileName}-api-router.ts`), apis.trim(), "utf8");
+
+let api = fs.readFileSync(path.join(__dirname, "../src/routes/apis", "index.ts"), "utf8");
+api = api.replace("\nconst api = express.Router();", `import ${varName}ApiRouter from "./${fileName}-api-router";\n\nconst api = express.Router();`);
+api = api.replace("export default api;", `api.use("/${fileName}", ${varName}ApiRouter);\n\nexport default api;`);
+fs.writeFileSync(path.join(__dirname, "../src/routes/apis", "index.ts"), api, "utf8");
+
+console.log(`${fullName} generated`);
diff --git a/worklenz-backend/cli/generate-validator b/worklenz-backend/cli/generate-validator
new file mode 100644
index 00000000..b77bbb04
--- /dev/null
+++ b/worklenz-backend/cli/generate-validator
@@ -0,0 +1,27 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+const path = require("path");
+
+const arg = process.argv[2];
+const name = arg.trim().toLowerCase();
+
+const content = `
+import { NextFunction } from "express";
+
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+
+export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void {
+ const { example_name } = req.body;
+ if (!example_name)
+ return res.status(200).send(new ServerResponse(false, null, "Name is required"));
+ return next();
+}
+`;
+
+const fullName = `${name}-validator`;
+fs.writeFileSync(path.join(__dirname, "../src/middlewares/validators", `${fullName}.ts`), content.trim(), "utf8");
+
+console.log(`${fullName} generated`);
diff --git a/worklenz-backend/cli/inline-queries b/worklenz-backend/cli/inline-queries
new file mode 100644
index 00000000..eeaf8eca
--- /dev/null
+++ b/worklenz-backend/cli/inline-queries
@@ -0,0 +1,19 @@
+#!/usr/bin/env node
+
+const fs = require("fs");
+const path = require("path");
+
+// Preview
+
+function inline(folder) {
+ const controllers = fs.readdirSync(path.join(__dirname, folder)).filter(f => f.split(".").pop() === "js");
+ const replacer = (match, p1, p2, p3, offset, string) => match.split(/\n/g).map(s => s.trim()).join(" ").trim();
+ for (const item of controllers) {
+ const controller = fs.readFileSync(path.join(__dirname, folder, item), "utf8");
+ const q = controller.replace(/(?<=q\s+=(.*?)`)([\s\S]*?)(?=`;)/g, replacer);
+ fs.writeFileSync(path.join(__dirname, folder, item), q, "utf8");
+ }
+}
+
+// inline("../build/controllers");
+// inline("../build/passport");
diff --git a/worklenz-backend/cli/mkrelease b/worklenz-backend/cli/mkrelease
new file mode 100644
index 00000000..82523da2
--- /dev/null
+++ b/worklenz-backend/cli/mkrelease
@@ -0,0 +1,111 @@
+#!/usr/bin/env node
+
+/* eslint-disable no-console */
+const fs = require("fs");
+const path = require("path");
+const { ncp } = require("ncp");
+const { exec } = require("child_process");
+
+require("dotenv").config();
+
+const options = {
+ angular_dist_dir: process.env.ANGULAR_DIST_DIR,
+ angular_src_dir: process.env.ANGULAR_SRC_DIR,
+ backend_public_dir: process.env.BACKEND_PUBLIC_DIR,
+ backend_views_dir: process.env.BACKEND_VIEWS_DIR,
+ commit_changes: process.env.COMMIT_BUILD_IMMEDIATELY === "true"
+};
+
+function run_git_commit(version) {
+ if (!options.commit_changes) return;
+ const message = `Frontend build (${version})`;
+ console.log(message);
+ exec(`git add . && git commit -m "build: ${message}"`);
+}
+
+function copy_build() {
+ const deleteFolderRecursive = function (p) {
+ if (fs.existsSync(p)) {
+ fs.readdirSync(p).forEach((file) => {
+ const curPath = path.join(p, file);
+ if (fs.lstatSync(curPath).isDirectory()) { // recurse
+ deleteFolderRecursive(curPath);
+ } else { // delete file
+ fs.unlinkSync(curPath);
+ }
+ });
+ fs.rmdirSync(p);
+ }
+ };
+
+ if (fs.existsSync(path.join(options.angular_dist_dir, "index.html"))) {
+ const styles = /styles\.\w+\.css/g;
+ const runtime = /runtime\.\w+\.js/g;
+ const polyfills = /polyfills\.\w+\.js/g;
+ // const scripts = /scripts\.\w+\.js/g;
+ const main = /main\.\w+\.js/g;
+
+ const version = /\?v=\d+/g;
+
+ const layoutPath = path.join(options.backend_views_dir, "layout.pug");
+ const indexHtml = fs.readFileSync(path.join(options.angular_dist_dir, "index.html"), "utf8");
+ const pugLayout = fs.readFileSync(layoutPath, "utf8");
+
+ const v = Date.now();
+
+ const newLayout = pugLayout
+ .replace(styles, indexHtml.match(styles)[0])
+ .replace(runtime, indexHtml.match(runtime)[0])
+ .replace(polyfills, indexHtml.match(polyfills)[0])
+ // .replace(scripts, indexHtml.match(scripts)[0])
+ .replace(main, indexHtml.match(main)[0])
+ .replace(version, `?v=${v}`);
+
+ fs.writeFileSync(layoutPath, newLayout, "utf8");
+
+ if (fs.existsSync(options.backend_public_dir)) deleteFolderRecursive(options.backend_public_dir);
+ fs.mkdirSync(options.backend_public_dir);
+
+ ncp(options.angular_dist_dir, options.backend_public_dir, (err) => {
+ if (err) {
+ return console.error(err);
+ }
+ fs.unlinkSync(path.join(options.backend_public_dir, "index.html"));
+
+ const versionFile = path.join(__dirname, "../", "release");
+ let $v = null;
+ if (fs.existsSync(versionFile)) {
+ const version = fs.readFileSync(versionFile, "utf8");
+ $v = +version + 1;
+ fs.writeFileSync(versionFile, $v.toString(), "utf8");
+ }
+ console.log(`Release Updated - v${$v}`);
+ console.log("Running git commit.");
+ setTimeout(() => run_git_commit(`v${$v}`), 500);
+ });
+ } else {
+ console.log("Build does not exists!");
+ }
+
+}
+
+function build() {
+ console.log("Start building the Angular Project");
+ const cmd = exec(`cd ${options.angular_src_dir} && npm run build`);
+
+ cmd.stdout.on("data", (data) => {
+ console.log(data.toString());
+ });
+
+ cmd.stderr.on("data", (data) => {
+ console.log(`${data.toString()}`);
+ });
+
+ cmd.on("exit", (code) => {
+ console.log("Build success.");
+ console.log("Start Coping");
+ setTimeout(copy_build, 500);
+ });
+}
+
+build();
diff --git a/worklenz-backend/cli/swagger b/worklenz-backend/cli/swagger
new file mode 100644
index 00000000..640d6ba9
--- /dev/null
+++ b/worklenz-backend/cli/swagger
@@ -0,0 +1,21 @@
+#!/usr/bin/env node
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+const swaggerJsdoc = require("swagger-jsdoc");
+const fs = require("fs");
+const path = require("path");
+
+const options = {
+ definition: {
+ openapi: "3.0.0",
+ info: {
+ title: "Hello World",
+ version: "1.0.0",
+ },
+ },
+ apis: ["../build/routes/*.js"], // files containing annotations as above
+};
+
+const openapiSpecification = swaggerJsdoc(options);
+
+fs.writeFileSync(path.join(__dirname, "../build/swagger.json"), JSON.stringify(openapiSpecification), "utf8");
diff --git a/worklenz-backend/database/1_tables.sql b/worklenz-backend/database/1_tables.sql
new file mode 100644
index 00000000..7a5c3057
--- /dev/null
+++ b/worklenz-backend/database/1_tables.sql
@@ -0,0 +1,1901 @@
+-- CREATE DATABASE worklenz_db;
+
+-- Extensions
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+CREATE EXTENSION IF NOT EXISTS "unaccent";
+
+-- Domains
+CREATE DOMAIN WL_HEX_COLOR AS TEXT CHECK (value ~* '^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$');
+CREATE DOMAIN WL_EMAIL AS TEXT CHECK (value ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
+
+-- Enumerated Types
+-- Add new values using "ALTER TYPE WL_TASK_LIST_COL_KEY ADD VALUE 'NEW_VALUE_NAME' AFTER 'REPORTER';"
+CREATE TYPE WL_TASK_LIST_COL_KEY AS ENUM ('ASSIGNEES', 'COMPLETED_DATE', 'CREATED_DATE', 'DESCRIPTION', 'DUE_DATE', 'ESTIMATION', 'KEY', 'LABELS', 'LAST_UPDATED', 'NAME', 'PRIORITY', 'PROGRESS', 'START_DATE', 'STATUS', 'TIME_TRACKING', 'REPORTER', 'PHASE');
+
+
+CREATE TABLE archived_projects (
+ user_id UUID NOT NULL,
+ project_id UUID NOT NULL
+);
+
+ALTER TABLE archived_projects
+ ADD CONSTRAINT archived_projects_pk
+ PRIMARY KEY (user_id, project_id);
+
+CREATE TABLE bounced_emails (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ email TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX bounced_emails_email_uindex
+ ON bounced_emails (email);
+
+ALTER TABLE bounced_emails
+ ADD CONSTRAINT bounced_emails_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE clients (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ team_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE INDEX clients_id_team_id_index
+ ON clients (id, team_id);
+
+CREATE UNIQUE INDEX clients_name_team_id_uindex
+ ON clients (name, team_id);
+
+ALTER TABLE clients
+ ADD CONSTRAINT clients_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE clients
+ ADD CONSTRAINT clients_name_check
+ CHECK (CHAR_LENGTH(name) <= 60);
+
+CREATE TABLE countries (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ code CHAR(2) NOT NULL,
+ name VARCHAR(150) NOT NULL,
+ phone INTEGER NOT NULL,
+ currency VARCHAR(3) DEFAULT NULL::CHARACTER VARYING
+);
+
+ALTER TABLE countries
+ ADD PRIMARY KEY (id);
+
+CREATE TABLE cpt_phases (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ template_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX cpt_phases_name_project_uindex
+ ON cpt_phases (name, template_id);
+
+ALTER TABLE cpt_phases
+ ADD CONSTRAINT cpt_phases_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE cpt_task_labels (
+ task_id UUID NOT NULL,
+ label_id UUID NOT NULL
+);
+
+ALTER TABLE cpt_task_labels
+ ADD CONSTRAINT cpt_task_labels_pk
+ PRIMARY KEY (task_id, label_id);
+
+CREATE TABLE cpt_task_phases (
+ task_id UUID NOT NULL,
+ phase_id UUID NOT NULL
+);
+
+CREATE UNIQUE INDEX cpt_task_phase_cpt_task_phase_uindex
+ ON cpt_task_phases (task_id, phase_id);
+
+CREATE UNIQUE INDEX cpt_task_phase_task_id_uindex
+ ON cpt_task_phases (task_id);
+
+ALTER TABLE cpt_task_phases
+ ADD CONSTRAINT cpt_task_phase_phase_id_fk
+ FOREIGN KEY (phase_id) REFERENCES cpt_phases
+ ON DELETE CASCADE;
+
+CREATE TABLE cpt_task_statuses (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ template_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ category_id UUID NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL
+);
+
+CREATE UNIQUE INDEX cpt_task_statuses_template_id_name_uindex
+ ON cpt_task_statuses (template_id, name);
+
+ALTER TABLE cpt_task_statuses
+ ADD CONSTRAINT cpt_task_statuses_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE cpt_tasks (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ total_minutes NUMERIC DEFAULT 0 NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL,
+ task_no BIGINT,
+ original_task_id UUID,
+ priority_id UUID NOT NULL,
+ template_id UUID NOT NULL,
+ parent_task_id UUID,
+ status_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+COMMENT ON COLUMN cpt_tasks.original_task_id IS 'original_task_id from the project the template is created from';
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE cpt_task_labels
+ ADD CONSTRAINT cpt_task_labels_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES cpt_tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE cpt_task_phases
+ ADD CONSTRAINT cpt_task_phase_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES cpt_tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_sort_order_unique
+ UNIQUE (template_id, sort_order)
+ DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_status_id_fk
+ FOREIGN KEY (status_id) REFERENCES cpt_task_statuses
+ ON DELETE RESTRICT;
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_task_order_check
+ CHECK (sort_order >= 0);
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_total_minutes_check
+ CHECK ((total_minutes >= (0)::NUMERIC) AND (total_minutes <= (999999)::NUMERIC));
+
+CREATE TABLE custom_project_templates (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ phase_label TEXT DEFAULT 'Phase'::TEXT NOT NULL,
+ team_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ color_code TEXT NOT NULL,
+ notes TEXT
+);
+
+CREATE UNIQUE INDEX custom_project_templates_name_team_id_uindex
+ ON custom_project_templates (name, team_id);
+
+ALTER TABLE custom_project_templates
+ ADD CONSTRAINT custom_project_templates_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE cpt_phases
+ ADD CONSTRAINT cpt_phases_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES custom_project_templates
+ ON DELETE CASCADE;
+
+ALTER TABLE cpt_task_statuses
+ ADD CONSTRAINT cpt_task_statuses_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES custom_project_templates
+ ON DELETE CASCADE;
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_template_fk
+ FOREIGN KEY (template_id) REFERENCES custom_project_templates
+ ON DELETE CASCADE;
+
+CREATE TABLE email_invitations (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ email WL_EMAIL NOT NULL,
+ team_id UUID,
+ team_member_id UUID,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+ALTER TABLE email_invitations
+ ADD CONSTRAINT email_invitations_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE favorite_projects (
+ user_id UUID NOT NULL,
+ project_id UUID NOT NULL
+);
+
+ALTER TABLE favorite_projects
+ ADD CONSTRAINT favorite_projects_pk
+ PRIMARY KEY (user_id, project_id);
+
+CREATE TABLE job_titles (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ team_id UUID NOT NULL
+);
+
+CREATE UNIQUE INDEX job_titles_name_team_id_uindex
+ ON job_titles (name, team_id);
+
+CREATE INDEX job_titles_team_id_index
+ ON job_titles (team_id);
+
+ALTER TABLE job_titles
+ ADD CONSTRAINT job_titles_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE job_titles
+ ADD CONSTRAINT job_titles_name_check
+ CHECK (CHAR_LENGTH(name) <= 55);
+
+CREATE TABLE notification_settings (
+ email_notifications_enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ popup_notifications_enabled BOOLEAN DEFAULT TRUE NOT NULL,
+ show_unread_items_count BOOLEAN DEFAULT TRUE NOT NULL,
+ daily_digest_enabled BOOLEAN DEFAULT FALSE NOT NULL,
+ user_id UUID NOT NULL,
+ team_id UUID NOT NULL
+);
+
+CREATE INDEX notification_settings_team_user_id_index
+ ON notification_settings (team_id, user_id);
+
+ALTER TABLE notification_settings
+ ADD CONSTRAINT notification_settings_pk
+ PRIMARY KEY (user_id, team_id);
+
+CREATE TABLE organizations (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ organization_name TEXT NOT NULL,
+ contact_number TEXT,
+ contact_number_secondary TEXT,
+ address_line_1 TEXT,
+ address_line_2 TEXT,
+ country UUID,
+ city TEXT,
+ state TEXT,
+ postal_code TEXT,
+ trial_in_progress BOOLEAN DEFAULT FALSE NOT NULL,
+ trial_expire_date DATE,
+ subscription_status TEXT DEFAULT 'active'::TEXT NOT NULL,
+ storage INTEGER DEFAULT 1 NOT NULL,
+ updating_plan BOOLEAN DEFAULT FALSE,
+ user_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
+ license_type_id UUID
+);
+
+CREATE UNIQUE INDEX organizations_user_id_uindex
+ ON organizations (user_id);
+
+ALTER TABLE organizations
+ ADD CONSTRAINT organizations_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE organizations
+ ADD CONSTRAINT users_data_countries_id_fk
+ FOREIGN KEY (country) REFERENCES countries;
+
+ALTER TABLE organizations
+ ADD CONSTRAINT subscription_statuses_allowed
+ CHECK (subscription_status = ANY
+ (ARRAY ['active'::TEXT, 'past_due'::TEXT, 'trialing'::TEXT, 'paused'::TEXT, 'deleted'::TEXT, 'life_time_deal'::TEXT, 'free'::TEXT, 'custom'::TEXT, 'credit'::TEXT]));
+
+CREATE TABLE permissions (
+ id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL
+);
+
+CREATE UNIQUE INDEX permissions_name_uindex
+ ON permissions (name);
+
+ALTER TABLE permissions
+ ADD CONSTRAINT permissions_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE personal_todo_list (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ color_code WL_HEX_COLOR NOT NULL,
+ done BOOLEAN DEFAULT FALSE NOT NULL,
+ index INTEGER DEFAULT 0,
+ user_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX personal_todo_list_index_uindex
+ ON personal_todo_list (user_id, index);
+
+ALTER TABLE personal_todo_list
+ ADD CONSTRAINT personal_todo_list_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE personal_todo_list
+ ADD CONSTRAINT personal_todo_list_description_check
+ CHECK (CHAR_LENGTH(description) <= 200);
+
+ALTER TABLE personal_todo_list
+ ADD CONSTRAINT personal_todo_list_name_check
+ CHECK (CHAR_LENGTH(name) <= 100);
+
+CREATE TABLE pg_sessions (
+ sid VARCHAR NOT NULL,
+ sess JSON NOT NULL,
+ expire TIMESTAMP(6) NOT NULL
+);
+
+ALTER TABLE pg_sessions
+ ADD PRIMARY KEY (sid);
+
+CREATE TABLE project_access_levels (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ key TEXT NOT NULL
+);
+
+CREATE UNIQUE INDEX project_access_levels_key_uindex
+ ON project_access_levels (key);
+
+CREATE UNIQUE INDEX project_access_levels_name_uindex
+ ON project_access_levels (name);
+
+ALTER TABLE project_access_levels
+ ADD CONSTRAINT project_access_levels_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_categories (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR DEFAULT '#70a6f3'::TEXT NOT NULL,
+ team_id UUID NOT NULL,
+ created_by UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX project_categories_name_team_id_uindex
+ ON project_categories (name, team_id);
+
+ALTER TABLE project_categories
+ ADD CONSTRAINT project_categories_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_comment_mentions (
+ comment_id UUID NOT NULL,
+ mentioned_index INTEGER NOT NULL,
+ mentioned_by UUID NOT NULL,
+ informed_by UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE TABLE project_comments (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ content TEXT NOT NULL,
+ created_by UUID NOT NULL,
+ project_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE INDEX project_comments_project_id_index
+ ON project_comments (project_id);
+
+ALTER TABLE project_comments
+ ADD CONSTRAINT project_comments_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE project_comment_mentions
+ ADD CONSTRAINT project_comment_mentions_comment_id_fk
+ FOREIGN KEY (comment_id) REFERENCES project_comments
+ ON DELETE CASCADE;
+
+ALTER TABLE project_comments
+ ADD CONSTRAINT project_comments_content_length_check
+ CHECK (CHAR_LENGTH(content) <= 2000);
+
+CREATE TABLE project_folders (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ key TEXT NOT NULL,
+ color_code WL_HEX_COLOR DEFAULT '#70a6f3'::TEXT NOT NULL,
+ created_by UUID NOT NULL,
+ parent_folder_id UUID,
+ team_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX project_folders_team_id_key_uindex
+ ON project_folders (team_id, key);
+
+CREATE UNIQUE INDEX project_folders_team_id_name_uindex
+ ON project_folders (team_id, name);
+
+ALTER TABLE project_folders
+ ADD CONSTRAINT project_folders_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE project_folders
+ ADD CONSTRAINT project_folders_parent_folder_fk
+ FOREIGN KEY (parent_folder_id) REFERENCES project_folders;
+
+CREATE TABLE project_logs (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ team_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ description TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+ALTER TABLE project_logs
+ ADD CONSTRAINT project_logs_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_member_allocations (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ project_id UUID NOT NULL,
+ team_member_id UUID NOT NULL,
+ allocated_from TIMESTAMP WITH TIME ZONE NOT NULL,
+ allocated_to TIMESTAMP WITH TIME ZONE NOT NULL
+);
+
+ALTER TABLE project_member_allocations
+ ADD CONSTRAINT project_member_allocations_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_members (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ team_member_id UUID NOT NULL,
+ project_access_level_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ role_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ default_view TEXT DEFAULT 'TASK_LIST'::TEXT NOT NULL
+);
+
+CREATE INDEX project_members_project_id_index
+ ON project_members (project_id);
+
+CREATE INDEX project_members_project_id_member_id_index
+ ON project_members (project_id, team_member_id);
+
+CREATE INDEX project_members_team_member_id_index
+ ON project_members (team_member_id);
+
+CREATE UNIQUE INDEX project_members_team_member_project_uindex
+ ON project_members (team_member_id, project_id);
+
+ALTER TABLE project_members
+ ADD CONSTRAINT project_members_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE project_members
+ ADD CONSTRAINT project_members_access_level_fk
+ FOREIGN KEY (project_access_level_id) REFERENCES project_access_levels;
+
+CREATE TABLE project_phases (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ project_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ start_date TIMESTAMP WITH TIME ZONE,
+ end_date TIMESTAMP WITH TIME ZONE,
+ sort_index INTEGER DEFAULT 0
+);
+
+CREATE UNIQUE INDEX project_phases_name_project_uindex
+ ON project_phases (name, project_id);
+
+ALTER TABLE project_phases
+ ADD CONSTRAINT project_phases_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_subscribers (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ user_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ team_member_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX project_subscribers_user_task_team_member_uindex
+ ON project_subscribers (user_id, project_id, team_member_id);
+
+ALTER TABLE project_subscribers
+ ADD CONSTRAINT project_subscribers_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE project_task_list_cols (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ key WL_TASK_LIST_COL_KEY NOT NULL,
+ index INTEGER DEFAULT 0 NOT NULL,
+ pinned BOOLEAN DEFAULT TRUE NOT NULL,
+ project_id UUID NOT NULL
+);
+
+CREATE INDEX project_task_list_cols_index
+ ON project_task_list_cols (project_id, index);
+
+CREATE UNIQUE INDEX project_task_list_cols_key_project_uindex
+ ON project_task_list_cols (key, project_id);
+
+ALTER TABLE project_task_list_cols
+ ADD CONSTRAINT project_task_list_cols_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE projects (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ key TEXT NOT NULL,
+ color_code WL_HEX_COLOR DEFAULT '#70a6f3'::TEXT NOT NULL,
+ notes TEXT,
+ tasks_counter BIGINT DEFAULT 0 NOT NULL,
+ start_date TIMESTAMP WITH TIME ZONE,
+ end_date TIMESTAMP WITH TIME ZONE,
+ team_id UUID NOT NULL,
+ client_id UUID,
+ owner_id UUID NOT NULL,
+ status_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ category_id UUID,
+ folder_id UUID,
+ phase_label TEXT DEFAULT 'Phase'::TEXT NOT NULL,
+ estimated_man_days INTEGER DEFAULT 0,
+ hours_per_day INTEGER DEFAULT 8,
+ health_id UUID,
+ estimated_working_days INTEGER DEFAULT 0
+);
+
+CREATE INDEX projects_folder_id_index
+ ON projects (folder_id);
+
+CREATE INDEX projects_id_team_id_index
+ ON projects (id, team_id);
+
+CREATE UNIQUE INDEX projects_key_team_id_uindex
+ ON projects (key, team_id);
+
+CREATE INDEX projects_name_index
+ ON projects (name);
+
+CREATE UNIQUE INDEX projects_name_team_id_uindex
+ ON projects (name, team_id);
+
+CREATE INDEX projects_team_id_folder_id_index
+ ON projects (team_id, folder_id);
+
+CREATE INDEX projects_team_id_index
+ ON projects (team_id);
+
+CREATE INDEX projects_team_id_name_index
+ ON projects (team_id, name);
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE archived_projects
+ ADD CONSTRAINT archived_projects_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE favorite_projects
+ ADD CONSTRAINT favorite_projects_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_comments
+ ADD CONSTRAINT project_comments_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_logs
+ ADD CONSTRAINT project_logs_projects_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_member_allocations
+ ADD CONSTRAINT project_members_allocations_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_members
+ ADD CONSTRAINT project_members_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_phases
+ ADD CONSTRAINT project_phases_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_subscribers
+ ADD CONSTRAINT project_subscribers_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE project_task_list_cols
+ ADD CONSTRAINT project_task_list_cols_project_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_category_id_fk
+ FOREIGN KEY (category_id) REFERENCES project_categories
+ ON DELETE CASCADE;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_client_id_fk
+ FOREIGN KEY (client_id) REFERENCES clients
+ ON DELETE SET NULL;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_folder_id_fk
+ FOREIGN KEY (folder_id) REFERENCES project_folders
+ ON DELETE SET DEFAULT;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_name_check
+ CHECK (CHAR_LENGTH(name) <= 100);
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_notes_check
+ CHECK (CHAR_LENGTH(notes) <= 500);
+
+CREATE TABLE pt_labels (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code TEXT NOT NULL,
+ template_id UUID
+);
+
+ALTER TABLE pt_labels
+ ADD CONSTRAINT pt_project_templates_labels_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE pt_phases (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code TEXT,
+ template_id UUID NOT NULL
+);
+
+ALTER TABLE pt_phases
+ ADD CONSTRAINT pt_project_template_phases_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE pt_project_templates (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ key TEXT NOT NULL,
+ description TEXT,
+ phase_label TEXT,
+ image_url TEXT,
+ color_code TEXT DEFAULT '#3b7ad4'::TEXT NOT NULL
+);
+
+ALTER TABLE pt_project_templates
+ ADD CONSTRAINT pt_project_templates_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE pt_labels
+ ADD CONSTRAINT pt_labels_pt_project_templates_id_fk
+ FOREIGN KEY (template_id) REFERENCES pt_project_templates;
+
+ALTER TABLE pt_phases
+ ADD CONSTRAINT pt_project_template_phases_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES pt_project_templates
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_project_templates
+ ADD CONSTRAINT pt_project_templates_key_unique
+ UNIQUE (key);
+
+CREATE TABLE pt_statuses (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ template_id UUID NOT NULL,
+ category_id UUID NOT NULL
+);
+
+ALTER TABLE pt_statuses
+ ADD CONSTRAINT pt_project_template_statuses_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE pt_statuses
+ ADD CONSTRAINT pt_project_template_statuses_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES pt_project_templates
+ ON DELETE CASCADE;
+
+CREATE TABLE pt_task_labels (
+ task_id UUID NOT NULL,
+ label_id UUID NOT NULL
+);
+
+ALTER TABLE pt_task_labels
+ ADD CONSTRAINT pt_task_labels_pk
+ PRIMARY KEY (task_id, label_id);
+
+ALTER TABLE pt_task_labels
+ ADD CONSTRAINT pt_task_labels_label_id_fk
+ FOREIGN KEY (label_id) REFERENCES pt_labels;
+
+CREATE TABLE pt_task_phases (
+ task_id UUID NOT NULL,
+ phase_id UUID NOT NULL
+);
+
+CREATE UNIQUE INDEX pt_task_phase_pt_task_phase_uindex
+ ON pt_task_phases (task_id, phase_id);
+
+CREATE UNIQUE INDEX pt_task_phase_task_id_uindex
+ ON pt_task_phases (task_id);
+
+ALTER TABLE pt_task_phases
+ ADD CONSTRAINT pt_task_phase_phase_id_fk
+ FOREIGN KEY (phase_id) REFERENCES pt_phases
+ ON DELETE CASCADE;
+
+CREATE TABLE pt_task_statuses (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ template_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ category_id UUID NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL
+);
+
+CREATE UNIQUE INDEX pt_task_statuses_template_id_name_uindex
+ ON pt_task_statuses (template_id, name);
+
+ALTER TABLE pt_task_statuses
+ ADD CONSTRAINT pt_task_statuses_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE pt_task_statuses
+ ADD CONSTRAINT pt_task_statuses_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES pt_project_templates
+ ON DELETE CASCADE;
+
+CREATE TABLE pt_tasks (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ total_minutes NUMERIC DEFAULT 0 NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL,
+ priority_id UUID NOT NULL,
+ template_id UUID NOT NULL,
+ parent_task_id UUID,
+ status_id UUID NOT NULL
+);
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE pt_task_labels
+ ADD CONSTRAINT pt_task_labels_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES pt_tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_task_phases
+ ADD CONSTRAINT pt_task_phase_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES pt_tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_sort_order_unique
+ UNIQUE (template_id, sort_order)
+ DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_parent_task_id_fk
+ FOREIGN KEY (parent_task_id) REFERENCES pt_tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_status_id_fk
+ FOREIGN KEY (status_id) REFERENCES pt_statuses
+ ON DELETE RESTRICT;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_template_fk
+ FOREIGN KEY (template_id) REFERENCES pt_project_templates
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_task_order_check
+ CHECK (sort_order >= 0);
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_total_minutes_check
+ CHECK ((total_minutes >= (0)::NUMERIC) AND (total_minutes <= (999999)::NUMERIC));
+
+CREATE TABLE role_permissions (
+ role_id UUID NOT NULL,
+ permission_id TEXT NOT NULL
+);
+
+ALTER TABLE role_permissions
+ ADD CONSTRAINT role_permissions_pk
+ PRIMARY KEY (role_id, permission_id);
+
+ALTER TABLE role_permissions
+ ADD CONSTRAINT role_permissions_permission_id_fk
+ FOREIGN KEY (permission_id) REFERENCES permissions;
+
+CREATE TABLE roles (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ team_id UUID NOT NULL,
+ default_role BOOLEAN DEFAULT FALSE NOT NULL,
+ admin_role BOOLEAN DEFAULT FALSE NOT NULL,
+ owner BOOLEAN DEFAULT FALSE NOT NULL
+);
+
+CREATE UNIQUE INDEX roles_default_uindex
+ ON roles (team_id)
+ WHERE (default_role IS TRUE);
+
+CREATE UNIQUE INDEX roles_name_team_id_uindex
+ ON roles (name, team_id);
+
+CREATE UNIQUE INDEX roles_owner_uindex
+ ON roles (team_id)
+ WHERE (owner IS TRUE);
+
+ALTER TABLE roles
+ ADD CONSTRAINT roles_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE project_members
+ ADD CONSTRAINT project_members_role_id_fk
+ FOREIGN KEY (role_id) REFERENCES roles;
+
+ALTER TABLE role_permissions
+ ADD CONSTRAINT role_permissions_role_id_fk
+ FOREIGN KEY (role_id) REFERENCES roles;
+
+CREATE TABLE spam_emails (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ email TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX spam_emails_email_uindex
+ ON spam_emails (email);
+
+ALTER TABLE spam_emails
+ ADD CONSTRAINT spam_emails_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE sys_project_healths (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL,
+ is_default BOOLEAN DEFAULT FALSE NOT NULL
+);
+
+ALTER TABLE sys_project_healths
+ ADD CONSTRAINT sys_project_healths_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_sys_project_healths_id_fk
+ FOREIGN KEY (health_id) REFERENCES sys_project_healths;
+
+CREATE TABLE sys_project_statuses (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ icon TEXT NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL,
+ is_default BOOLEAN DEFAULT FALSE NOT NULL
+);
+
+ALTER TABLE sys_project_statuses
+ ADD CONSTRAINT sys_project_statuses_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_status_id_fk
+ FOREIGN KEY (status_id) REFERENCES sys_project_statuses;
+
+CREATE TABLE sys_task_status_categories (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ index INTEGER DEFAULT 0 NOT NULL,
+ is_todo BOOLEAN DEFAULT FALSE NOT NULL,
+ is_doing BOOLEAN DEFAULT FALSE NOT NULL,
+ is_done BOOLEAN DEFAULT FALSE NOT NULL,
+ description TEXT
+);
+
+ALTER TABLE sys_task_status_categories
+ ADD CONSTRAINT sys_task_status_categories_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE cpt_task_statuses
+ ADD CONSTRAINT cpt_task_statuses_category_id_fk
+ FOREIGN KEY (category_id) REFERENCES sys_task_status_categories;
+
+ALTER TABLE pt_statuses
+ ADD CONSTRAINT pt_project_template_statuses_category_id_fk
+ FOREIGN KEY (category_id) REFERENCES sys_task_status_categories;
+
+ALTER TABLE pt_task_statuses
+ ADD CONSTRAINT pt_task_statuses_category_id_fk
+ FOREIGN KEY (category_id) REFERENCES sys_task_status_categories;
+
+CREATE TABLE task_activity_logs (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ task_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ attribute_type TEXT NOT NULL,
+ user_id UUID NOT NULL,
+ log_type TEXT,
+ old_value TEXT,
+ new_value TEXT,
+ prev_string TEXT,
+ next_string TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ project_id UUID NOT NULL
+);
+
+COMMENT ON COLUMN task_activity_logs.user_id IS 'id of the user who initiated the activity';
+
+COMMENT ON COLUMN task_activity_logs.log_type IS 'whether the log belongs to create, update, delete, assign or unassign category';
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_projects_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_log_type_check
+ CHECK (log_type = ANY (ARRAY ['create'::TEXT, 'update'::TEXT, 'delete'::TEXT, 'assign'::TEXT, 'unassign'::TEXT]));
+
+CREATE TABLE task_attachments (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ size BIGINT DEFAULT 0 NOT NULL,
+ type TEXT NOT NULL,
+ task_id UUID,
+ team_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ uploaded_by UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE INDEX task_attachments_task_id_index
+ ON task_attachments (task_id);
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_name_check
+ CHECK (CHAR_LENGTH(name) <= 110);
+
+CREATE TABLE task_comment_contents (
+ index INTEGER NOT NULL,
+ comment_id UUID NOT NULL,
+ team_member_id UUID,
+ text_content TEXT
+);
+
+ALTER TABLE task_comment_contents
+ ADD CONSTRAINT task_comment_contents_content_check
+ CHECK (((team_member_id IS NULL) AND (text_content IS NULL)) IS FALSE);
+
+ALTER TABLE task_comment_contents
+ ADD CONSTRAINT task_comment_contents_name_check
+ CHECK (CHAR_LENGTH(text_content) <= 2000);
+
+CREATE TABLE task_comments (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ user_id UUID NOT NULL,
+ team_member_id UUID NOT NULL,
+ task_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ ses_message_id TEXT
+);
+
+CREATE INDEX task_comments_task_id_index
+ ON task_comments (task_id);
+
+ALTER TABLE task_comments
+ ADD CONSTRAINT task_comments_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_comment_contents
+ ADD CONSTRAINT task_comment_contents_comment_id_fk
+ FOREIGN KEY (comment_id) REFERENCES task_comments
+ ON DELETE CASCADE;
+
+CREATE TABLE task_labels (
+ task_id UUID NOT NULL,
+ label_id UUID NOT NULL
+);
+
+CREATE INDEX task_labels_task_id_index
+ ON task_labels (task_id);
+
+ALTER TABLE task_labels
+ ADD CONSTRAINT task_labels_pk
+ PRIMARY KEY (task_id, label_id);
+
+CREATE TABLE team_labels (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL,
+ team_id UUID NOT NULL
+);
+
+CREATE INDEX team_labels_name_index
+ ON team_labels (name);
+
+CREATE UNIQUE INDEX team_labels_name_team_uindex
+ ON team_labels (name, team_id);
+
+ALTER TABLE team_labels
+ ADD CONSTRAINT team_labels_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE cpt_task_labels
+ ADD CONSTRAINT cpt_task_labels_label_id_fk
+ FOREIGN KEY (label_id) REFERENCES team_labels
+ ON DELETE CASCADE;
+
+ALTER TABLE task_labels
+ ADD CONSTRAINT task_labels_label_id_fk
+ FOREIGN KEY (label_id) REFERENCES team_labels
+ ON DELETE CASCADE;
+
+ALTER TABLE team_labels
+ ADD CONSTRAINT team_labels_name_check
+ CHECK (CHAR_LENGTH(name) <= 40);
+
+CREATE TABLE task_phase (
+ task_id UUID NOT NULL,
+ phase_id UUID NOT NULL
+);
+
+CREATE UNIQUE INDEX task_phase_task_id_uindex
+ ON task_phase (task_id);
+
+CREATE UNIQUE INDEX task_phase_task_phase_uindex
+ ON task_phase (task_id, phase_id);
+
+ALTER TABLE task_phase
+ ADD CONSTRAINT task_phase_phase_id_fk
+ FOREIGN KEY (phase_id) REFERENCES project_phases
+ ON DELETE CASCADE;
+
+CREATE TABLE task_priorities (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ value INTEGER DEFAULT 0 NOT NULL,
+ color_code WL_HEX_COLOR NOT NULL
+);
+
+CREATE UNIQUE INDEX task_priorities_name_uindex
+ ON task_priorities (name);
+
+ALTER TABLE task_priorities
+ ADD CONSTRAINT task_priorities_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE cpt_tasks
+ ADD CONSTRAINT cpt_tasks_priority_fk
+ FOREIGN KEY (priority_id) REFERENCES task_priorities;
+
+ALTER TABLE pt_tasks
+ ADD CONSTRAINT pt_tasks_priority_fk
+ FOREIGN KEY (priority_id) REFERENCES task_priorities;
+
+CREATE TABLE task_statuses (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ project_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ category_id UUID NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX task_statuses_name_index
+ ON task_statuses (name);
+
+CREATE INDEX task_statuses_project_category_index
+ ON task_statuses (project_id, category_id);
+
+CREATE UNIQUE INDEX task_statuses_project_id_name_uindex
+ ON task_statuses (project_id, name);
+
+ALTER TABLE task_statuses
+ ADD CONSTRAINT task_statuses_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_statuses
+ ADD CONSTRAINT task_statuses_category_id_fk
+ FOREIGN KEY (category_id) REFERENCES sys_task_status_categories;
+
+ALTER TABLE task_statuses
+ ADD CONSTRAINT task_statuses_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE task_statuses
+ ADD CONSTRAINT task_statuses_name_check
+ CHECK (CHAR_LENGTH(name) <= 50);
+
+CREATE TABLE task_subscribers (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ user_id UUID NOT NULL,
+ task_id UUID NOT NULL,
+ team_member_id UUID NOT NULL,
+ action TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE INDEX task_subscribers_task_id_index
+ ON task_subscribers (task_id);
+
+CREATE INDEX task_subscribers_user_id_index
+ ON task_subscribers (user_id);
+
+CREATE UNIQUE INDEX task_subscribers_user_task_team_member_uindex
+ ON task_subscribers (user_id, task_id, team_member_id);
+
+ALTER TABLE task_subscribers
+ ADD CONSTRAINT task_subscribers_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_subscribers
+ ADD CONSTRAINT task_subscribers_action_check
+ CHECK (action = 'WHEN_DONE'::TEXT);
+
+CREATE TABLE task_templates (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ team_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX task_templates_name_team_uindex
+ ON task_templates (name, team_id);
+
+ALTER TABLE task_templates
+ ADD CONSTRAINT task_templates_pk
+ PRIMARY KEY (id);
+
+CREATE TABLE task_templates_tasks (
+ name TEXT NOT NULL,
+ template_id UUID NOT NULL,
+ total_minutes NUMERIC DEFAULT 0 NOT NULL
+);
+
+ALTER TABLE task_templates_tasks
+ ADD CONSTRAINT task_templates_tasks_template_id_fk
+ FOREIGN KEY (template_id) REFERENCES task_templates
+ ON DELETE CASCADE;
+
+CREATE TABLE task_timers (
+ task_id UUID NOT NULL,
+ user_id UUID NOT NULL,
+ start_time TIMESTAMP WITH TIME ZONE
+);
+
+CREATE INDEX task_timers_task_id_user_id_index
+ ON task_timers (task_id, user_id);
+
+ALTER TABLE task_timers
+ ADD CONSTRAINT task_timers_pk
+ PRIMARY KEY (task_id, user_id);
+
+CREATE TABLE task_updates (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ type TEXT NOT NULL,
+ reporter_id UUID NOT NULL,
+ task_id UUID NOT NULL,
+ user_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ is_sent BOOLEAN DEFAULT FALSE NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_type_check
+ CHECK (type = ANY (ARRAY ['ASSIGN'::TEXT, 'UNASSIGN'::TEXT]));
+
+CREATE TABLE task_work_log (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ time_spent NUMERIC DEFAULT 0 NOT NULL,
+ description TEXT,
+ logged_by_timer BOOLEAN DEFAULT FALSE NOT NULL,
+ task_id UUID NOT NULL,
+ user_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+ALTER TABLE task_work_log
+ ADD CONSTRAINT task_work_log_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_work_log
+ ADD CONSTRAINT task_work_log_description_check
+ CHECK (CHAR_LENGTH(description) <= 500);
+
+ALTER TABLE task_work_log
+ ADD CONSTRAINT task_work_log_time_spent_check
+ CHECK (time_spent >= (0)::NUMERIC);
+
+CREATE TABLE tasks (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ done BOOLEAN DEFAULT FALSE NOT NULL,
+ total_minutes NUMERIC DEFAULT 0 NOT NULL,
+ archived BOOLEAN DEFAULT FALSE NOT NULL,
+ task_no BIGINT NOT NULL,
+ start_date TIMESTAMP WITH TIME ZONE,
+ end_date TIMESTAMP WITH TIME ZONE,
+ priority_id UUID NOT NULL,
+ project_id UUID NOT NULL,
+ reporter_id UUID NOT NULL,
+ parent_task_id UUID,
+ status_id UUID NOT NULL,
+ completed_at TIMESTAMP WITH TIME ZONE,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ sort_order INTEGER DEFAULT 0 NOT NULL,
+ roadmap_sort_order INTEGER DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX tasks_created_at_index
+ ON tasks (created_at);
+
+CREATE INDEX tasks_id_project_id_index
+ ON tasks (id, project_id);
+
+CREATE INDEX tasks_name_index
+ ON tasks (name);
+
+CREATE INDEX tasks_parent_task_id_index
+ ON tasks (parent_task_id);
+
+CREATE INDEX tasks_project_id_index
+ ON tasks (project_id);
+
+CREATE INDEX tasks_sort_order_index
+ ON tasks (sort_order);
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_tasks_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE SET NULL;
+
+ALTER TABLE task_comments
+ ADD CONSTRAINT task_comments_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_labels
+ ADD CONSTRAINT task_labels_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_phase
+ ADD CONSTRAINT task_phase_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_subscribers
+ ADD CONSTRAINT task_subscribers_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_timers
+ ADD CONSTRAINT task_timers_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE task_work_log
+ ADD CONSTRAINT task_work_log_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_sort_order_unique
+ UNIQUE (project_id, sort_order)
+ DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_parent_task_id_fk
+ FOREIGN KEY (parent_task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_priority_fk
+ FOREIGN KEY (priority_id) REFERENCES task_priorities;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_project_fk
+ FOREIGN KEY (project_id) REFERENCES projects;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_status_id_fk
+ FOREIGN KEY (status_id) REFERENCES task_statuses
+ ON DELETE RESTRICT;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_description_check
+ CHECK (CHAR_LENGTH(description) <= 500000);
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_name_check
+ CHECK (CHAR_LENGTH(name) <= 500);
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_total_minutes_check
+ CHECK ((total_minutes >= (0)::NUMERIC) AND (total_minutes <= (999999)::NUMERIC));
+
+CREATE TABLE tasks_assignees (
+ task_id UUID NOT NULL,
+ project_member_id UUID NOT NULL,
+ team_member_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ assigned_by UUID NOT NULL
+);
+
+CREATE INDEX tasks_assignees_team_member_id_index
+ ON tasks_assignees (team_member_id);
+
+ALTER TABLE tasks_assignees
+ ADD CONSTRAINT tasks_assignees_pk
+ PRIMARY KEY (task_id, project_member_id);
+
+ALTER TABLE tasks_assignees
+ ADD CONSTRAINT tasks_assignees_project_member_id_fk
+ FOREIGN KEY (project_member_id) REFERENCES project_members
+ ON UPDATE CASCADE ON DELETE CASCADE;
+
+ALTER TABLE tasks_assignees
+ ADD CONSTRAINT tasks_assignees_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+CREATE TABLE team_members (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ user_id UUID,
+ team_id UUID NOT NULL,
+ role_id UUID NOT NULL,
+ job_title_id UUID,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ active BOOLEAN DEFAULT TRUE
+);
+
+CREATE INDEX team_members_ids_index
+ ON team_members (team_id, user_id, role_id);
+
+CREATE INDEX team_members_team_id_index
+ ON team_members (team_id);
+
+CREATE INDEX team_members_user_id_team_id_index
+ ON team_members (user_id, team_id);
+
+CREATE UNIQUE INDEX team_members_user_id_team_id_uindex
+ ON team_members (user_id, team_id);
+
+ALTER TABLE team_members
+ ADD CONSTRAINT team_members_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE email_invitations
+ ADD CONSTRAINT email_invitations_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE project_member_allocations
+ ADD CONSTRAINT project_members_allocations_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE project_members
+ ADD CONSTRAINT project_members_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE project_subscribers
+ ADD CONSTRAINT project_subscribers_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE task_comment_contents
+ ADD CONSTRAINT task_comment_contents_team_member_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members;
+
+ALTER TABLE task_comments
+ ADD CONSTRAINT task_comments_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE task_subscribers
+ ADD CONSTRAINT task_subscribers_team_member_id_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE tasks_assignees
+ ADD CONSTRAINT tasks_assignees_team_member_fk
+ FOREIGN KEY (team_member_id) REFERENCES team_members
+ ON DELETE CASCADE;
+
+ALTER TABLE team_members
+ ADD CONSTRAINT team_members_job_title_id_fk
+ FOREIGN KEY (job_title_id) REFERENCES job_titles
+ ON DELETE SET NULL;
+
+ALTER TABLE team_members
+ ADD CONSTRAINT team_members_role_id_fk
+ FOREIGN KEY (role_id) REFERENCES roles;
+
+-- START: Users
+CREATE SEQUENCE IF NOT EXISTS users_user_no_seq START 1;
+
+CREATE TABLE users (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ email WL_EMAIL NOT NULL,
+ password TEXT,
+ active_team UUID,
+ avatar_url TEXT,
+ setup_completed BOOLEAN DEFAULT FALSE NOT NULL,
+ user_no BIGINT DEFAULT NEXTVAL('users_user_no_seq'::REGCLASS) NOT NULL,
+ timezone_id UUID NOT NULL,
+ google_id TEXT,
+ socket_id TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ temp_email BOOLEAN DEFAULT FALSE
+);
+
+CREATE INDEX users_email_index
+ ON users (email);
+
+CREATE UNIQUE INDEX users_email_uindex
+ ON users (email);
+
+CREATE UNIQUE INDEX users_google_id_uindex
+ ON users (google_id);
+
+CREATE UNIQUE INDEX users_socket_id_uindex
+ ON users (socket_id);
+
+ALTER TABLE users
+ ADD CONSTRAINT users_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE archived_projects
+ ADD CONSTRAINT archived_projects_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE favorite_projects
+ ADD CONSTRAINT favorite_projects_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE notification_settings
+ ADD CONSTRAINT notification_settings_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE organizations
+ ADD CONSTRAINT organization_user_id_pk
+ FOREIGN KEY (user_id) REFERENCES users ON DELETE CASCADE;
+
+ALTER TABLE personal_todo_list
+ ADD CONSTRAINT personal_todo_list_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users ON DELETE CASCADE;
+
+ALTER TABLE project_categories
+ ADD CONSTRAINT project_categories_created_by_fk
+ FOREIGN KEY (created_by) REFERENCES users;
+
+ALTER TABLE project_comment_mentions
+ ADD CONSTRAINT project_comment_mentions_informed_by_fk
+ FOREIGN KEY (informed_by) REFERENCES users;
+
+ALTER TABLE project_comment_mentions
+ ADD CONSTRAINT project_comment_mentions_mentioned_by_fk
+ FOREIGN KEY (mentioned_by) REFERENCES users;
+
+ALTER TABLE project_comments
+ ADD CONSTRAINT project_comments_created_by_fk
+ FOREIGN KEY (created_by) REFERENCES users;
+
+ALTER TABLE project_folders
+ ADD CONSTRAINT project_folders_created_by_fk
+ FOREIGN KEY (created_by) REFERENCES users;
+
+ALTER TABLE project_subscribers
+ ADD CONSTRAINT project_subscribers_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_owner_id_fk
+ FOREIGN KEY (owner_id) REFERENCES users;
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_users_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_uploaded_by_fk
+ FOREIGN KEY (uploaded_by) REFERENCES users;
+
+ALTER TABLE task_comments
+ ADD CONSTRAINT task_comments_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE task_subscribers
+ ADD CONSTRAINT task_subscribers_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE task_timers
+ ADD CONSTRAINT task_timers_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_reporter_id_fk
+ FOREIGN KEY (reporter_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE task_work_log
+ ADD CONSTRAINT task_work_log_users_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE tasks
+ ADD CONSTRAINT tasks_reporter_id_fk
+ FOREIGN KEY (reporter_id) REFERENCES users;
+
+ALTER TABLE tasks_assignees
+ ADD CONSTRAINT tasks_assignees_assigned_by_fk
+ FOREIGN KEY (assigned_by) REFERENCES users;
+
+ALTER TABLE team_members
+ ADD CONSTRAINT team_members_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE users
+ ADD CONSTRAINT users_email_check
+ CHECK (CHAR_LENGTH((email)::TEXT) <= 255);
+
+ALTER TABLE users
+ ADD CONSTRAINT users_name_check
+ CHECK (CHAR_LENGTH(name) <= 55);
+
+CREATE TABLE teams (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ user_id UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ organization_id UUID
+);
+
+CREATE INDEX teams_id_user_id_index
+ ON teams (id, user_id);
+
+ALTER TABLE teams
+ ADD CONSTRAINT teams_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE clients
+ ADD CONSTRAINT clients_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE cpt_task_statuses
+ ADD CONSTRAINT cpt_task_statuses_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE custom_project_templates
+ ADD CONSTRAINT custom_project_templates_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE email_invitations
+ ADD CONSTRAINT email_invitations_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE job_titles
+ ADD CONSTRAINT job_titles_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams;
+
+ALTER TABLE notification_settings
+ ADD CONSTRAINT notification_settings_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE project_categories
+ ADD CONSTRAINT project_categories_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE project_folders
+ ADD CONSTRAINT project_folders_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams;
+
+ALTER TABLE project_logs
+ ADD CONSTRAINT project_logs_teams_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE projects
+ ADD CONSTRAINT projects_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE pt_task_statuses
+ ADD CONSTRAINT pt_task_statuses_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE task_activity_logs
+ ADD CONSTRAINT task_activity_logs_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE task_attachments
+ ADD CONSTRAINT task_attachments_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE team_labels
+ ADD CONSTRAINT team_labels_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE task_statuses
+ ADD CONSTRAINT task_statuses_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE task_templates
+ ADD CONSTRAINT task_templates_teams_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE task_updates
+ ADD CONSTRAINT task_updates_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE team_members
+ ADD CONSTRAINT team_members_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE users
+ ADD CONSTRAINT users_active_team_fk
+ FOREIGN KEY (active_team) REFERENCES teams;
+
+ALTER TABLE teams
+ ADD CONSTRAINT team_organization_id_pk
+ FOREIGN KEY (organization_id) REFERENCES organizations;
+
+ALTER TABLE teams
+ ADD CONSTRAINT teams_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+ALTER TABLE teams
+ ADD CONSTRAINT teams_name_check
+ CHECK (CHAR_LENGTH(name) <= 55);
+
+CREATE TABLE timezones (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ name TEXT NOT NULL,
+ abbrev TEXT NOT NULL,
+ utc_offset INTERVAL NOT NULL
+);
+
+CREATE INDEX timezones_name_index
+ ON timezones (name);
+
+ALTER TABLE timezones
+ ADD CONSTRAINT timezones_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE users
+ ADD CONSTRAINT users_timezone_id_fk
+ FOREIGN KEY (timezone_id) REFERENCES timezones;
+
+CREATE TABLE user_notifications (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ message TEXT NOT NULL,
+ user_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ read BOOLEAN DEFAULT FALSE NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ task_id UUID,
+ project_id UUID
+);
+
+ALTER TABLE user_notifications
+ ADD CONSTRAINT user_notifications_pk
+ PRIMARY KEY (id);
+
+ALTER TABLE user_notifications
+ ADD CONSTRAINT user_notifications_project_id_fk
+ FOREIGN KEY (project_id) REFERENCES projects
+ ON DELETE CASCADE;
+
+ALTER TABLE user_notifications
+ ADD CONSTRAINT user_notifications_task_id_fk
+ FOREIGN KEY (task_id) REFERENCES tasks
+ ON DELETE CASCADE;
+
+ALTER TABLE user_notifications
+ ADD CONSTRAINT user_notifications_team_id_fk
+ FOREIGN KEY (team_id) REFERENCES teams
+ ON DELETE CASCADE;
+
+ALTER TABLE user_notifications
+ ADD CONSTRAINT user_notifications_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+CREATE TABLE users_data (
+ user_id UUID NOT NULL,
+ organization_name TEXT NOT NULL,
+ contact_number TEXT,
+ contact_number_secondary TEXT,
+ address_line_1 TEXT,
+ address_line_2 TEXT,
+ country UUID,
+ city TEXT,
+ state TEXT,
+ postal_code TEXT,
+ trial_in_progress BOOLEAN DEFAULT FALSE NOT NULL,
+ trial_expire_date DATE,
+ subscription_status TEXT DEFAULT 'active'::TEXT NOT NULL,
+ storage INTEGER DEFAULT 1 NOT NULL,
+ updating_plan BOOLEAN DEFAULT FALSE
+);
+
+CREATE UNIQUE INDEX users_data_user_id_uindex
+ ON users_data (user_id);
+
+ALTER TABLE users_data
+ ADD UNIQUE (user_id);
+
+ALTER TABLE users_data
+ ADD CONSTRAINT users_data_user_id_fk
+ FOREIGN KEY (user_id) REFERENCES users
+ ON DELETE CASCADE;
+
+ALTER TABLE users_data
+ ADD CONSTRAINT users_data_users_id_fk
+ FOREIGN KEY (user_id) REFERENCES users;
+
+CREATE TABLE worklenz_alerts (
+ description TEXT NOT NULL,
+ type TEXT NOT NULL,
+ active BOOLEAN DEFAULT FALSE
+);
+
+ALTER TABLE worklenz_alerts
+ ADD CONSTRAINT worklenz_alerts_type_check
+ CHECK (type = ANY (ARRAY ['success'::TEXT, 'info'::TEXT, 'warning'::TEXT, 'error'::TEXT]));
+
+
+CREATE TABLE task_comment_mentions (
+ comment_id UUID NOT NULL,
+ mentioned_index INTEGER DEFAULT 0 NOT NULL,
+ mentioned_by UUID NOT NULL,
+ informed_by UUID NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+ALTER TABLE task_comment_mentions
+ ADD CONSTRAINT task_comment_mentions_comment_id_fk
+ FOREIGN KEY (comment_id) REFERENCES task_comments
+ ON DELETE CASCADE;
+
+ALTER TABLE task_comment_mentions
+ ADD CONSTRAINT task_comment_mentions_mentioned_by_fk
+ FOREIGN KEY (mentioned_by) REFERENCES users;
+
+ALTER TABLE task_comment_mentions
+ ADD CONSTRAINT task_comment_mentions_informed_by_fk
+ FOREIGN KEY (informed_by) REFERENCES team_members;
+
+CREATE TABLE email_logs (
+ id UUID DEFAULT uuid_generate_v4() NOT NULL,
+ email TEXT NOT NULL,
+ subject TEXT NOT NULL,
+ html TEXT NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/worklenz-backend/database/2_triggers.sql b/worklenz-backend/database/2_triggers.sql
new file mode 100644
index 00000000..bfbd5653
--- /dev/null
+++ b/worklenz-backend/database/2_triggers.sql
@@ -0,0 +1,161 @@
+-- Lowercase email
+CREATE OR REPLACE FUNCTION lower_email() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+
+ IF (is_null_or_empty(NEW.email) IS FALSE)
+ THEN
+ NEW.email = LOWER(TRIM(NEW.email));
+ END IF;
+
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS users_email_lower ON users;
+CREATE TRIGGER users_email_lower
+ BEFORE INSERT OR UPDATE
+ ON users
+EXECUTE FUNCTION lower_email();
+
+DROP TRIGGER IF EXISTS email_invitations_email_lower ON email_invitations;
+CREATE TRIGGER email_invitations_email_lower
+ BEFORE INSERT OR UPDATE
+ ON email_invitations
+EXECUTE FUNCTION lower_email();
+-- Lowercase email
+
+-- Set task completed date
+CREATE OR REPLACE FUNCTION task_status_change_trigger_fn() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+ IF EXISTS(SELECT 1
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = NEW.status_id)
+ AND is_done IS TRUE)
+ THEN
+ UPDATE tasks SET completed_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
+ ELSE
+ UPDATE tasks SET completed_at = NULL WHERE id = NEW.id;
+ END IF;
+
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER tasks_status_id_change
+ AFTER UPDATE OF status_id
+ ON tasks
+ FOR EACH ROW
+ WHEN (OLD.status_id IS DISTINCT FROM new.status_id)
+EXECUTE FUNCTION task_status_change_trigger_fn();
+-- Set task completed date
+
+-- Insert notification settings for new team members
+CREATE OR REPLACE FUNCTION notification_settings_insert_trigger_fn() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+
+ IF (NOT EXISTS(SELECT 1 FROM notification_settings WHERE team_id = NEW.team_id AND user_id = NEW.user_id)) AND
+ (is_null_or_empty(NEW.user_id) IS FALSE) AND (EXISTS(SELECT 1 FROM users WHERE id = NEW.user_id))
+ THEN
+ INSERT INTO notification_settings (popup_notifications_enabled, show_unread_items_count, user_id,
+ team_id)
+ VALUES (TRUE, TRUE, NEW.user_id, NEW.team_id);
+ END IF;
+
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS insert_notification_settings ON team_members;
+CREATE TRIGGER insert_notification_settings
+ AFTER INSERT
+ ON team_members
+ FOR EACH ROW
+EXECUTE FUNCTION notification_settings_insert_trigger_fn();
+-- Insert notification settings for new team members
+
+-- Delete notification settings when removing team members
+CREATE OR REPLACE FUNCTION notification_settings_delete_trigger_fn() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+ DELETE FROM notification_settings WHERE user_id = OLD.user_id AND team_id = OLD.team_id;
+ RETURN OLD;
+END
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS remove_notification_settings ON team_members;
+CREATE TRIGGER remove_notification_settings
+ BEFORE DELETE
+ ON team_members
+ FOR EACH ROW
+EXECUTE FUNCTION notification_settings_delete_trigger_fn();
+-- Delete notification settings when removing team members
+
+-- Set task updated at
+CREATE OR REPLACE FUNCTION set_task_updated_at_trigger_fn() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER set_task_updated_at
+ BEFORE UPDATE
+ ON tasks
+ FOR EACH ROW
+EXECUTE FUNCTION set_task_updated_at_trigger_fn();
+-- Set task updated at
+
+-- Update project tasks counter
+CREATE OR REPLACE FUNCTION update_project_tasks_counter_trigger_fn() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+
+ UPDATE projects SET tasks_counter = (tasks_counter + 1) WHERE id = NEW.project_id;
+ NEW.task_no = (SELECT tasks_counter FROM projects WHERE id = NEW.project_id);
+
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+DROP TRIGGER IF EXISTS projects_tasks_counter_trigger ON tasks;
+CREATE TRIGGER projects_tasks_counter_trigger
+ BEFORE INSERT
+ ON tasks
+ FOR EACH ROW
+EXECUTE FUNCTION update_project_tasks_counter_trigger_fn();
+-- Update project tasks counter
+
+-- Task status change trigger
+CREATE OR REPLACE FUNCTION tasks_task_subscriber_notify_done_trigger() RETURNS TRIGGER AS
+$$
+DECLARE
+BEGIN
+ IF (EXISTS(SELECT 1
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = NEW.status_id)
+ AND is_done IS TRUE))
+ THEN
+ PERFORM pg_notify('db_task_status_changed', NEW.id::TEXT);
+ END IF;
+
+ RETURN NEW;
+END
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE TRIGGER tasks_task_subscriber_notify_done
+ BEFORE UPDATE OF status_id
+ ON tasks
+ FOR EACH ROW
+ WHEN (OLD.status_id IS DISTINCT FROM NEW.status_id)
+EXECUTE FUNCTION tasks_task_subscriber_notify_done_trigger();
+-- Task status change trigger
diff --git a/worklenz-backend/database/3_system-data.sql b/worklenz-backend/database/3_system-data.sql
new file mode 100644
index 00000000..9a417cb9
--- /dev/null
+++ b/worklenz-backend/database/3_system-data.sql
@@ -0,0 +1,77 @@
+CREATE OR REPLACE FUNCTION sys_insert_task_priorities() RETURNS VOID AS
+$$
+BEGIN
+ INSERT INTO task_priorities (name, value, color_code) VALUES ('Low', 0, '#75c997');
+ INSERT INTO task_priorities (name, value, color_code) VALUES ('Medium', 1, '#fbc84c');
+ INSERT INTO task_priorities (name, value, color_code) VALUES ('High', 2, '#f37070');
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sys_insert_project_access_levels() RETURNS VOID AS
+$$
+BEGIN
+ INSERT INTO project_access_levels (name, key)
+ VALUES ('Admin', 'ADMIN');
+ INSERT INTO project_access_levels (name, key)
+ VALUES ('Member', 'MEMBER');
+ INSERT INTO project_access_levels (name, key)
+ VALUES ('Project Manager', 'PROJECT_MANAGER');
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sys_insert_task_status_categories() RETURNS VOID AS
+$$
+BEGIN
+ INSERT INTO sys_task_status_categories (name, color_code, index, is_todo)
+ VALUES ('To do', '#a9a9a9', 0, TRUE);
+ INSERT INTO sys_task_status_categories (name, color_code, index, is_doing)
+ VALUES ('Doing', '#70a6f3', 1, TRUE);
+ INSERT INTO sys_task_status_categories (name, color_code, index, is_done)
+ VALUES ('Done', '#75c997', 2, TRUE);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sys_insert_project_statuses() RETURNS VOID AS
+$$
+BEGIN
+ INSERT INTO sys_project_statuses (name, color_code, icon, sort_order, is_default)
+ VALUES ('Cancelled', '#f37070', 'close-circle', 0, FALSE),
+ ('Blocked', '#cbc8a1', 'stop', 1, FALSE),
+ ('On Hold', '#cbc8a1', 'stop', 2, FALSE),
+ ('Proposed', '#cbc8a1', 'clock-circle', 3, TRUE),
+ ('In Planning', '#cbc8a1', 'clock-circle', 4, FALSE),
+ ('In Progress', '#80ca79', 'clock-circle', 5, FALSE),
+ ('Completed', '#80ca79', 'check-circle', 6, FALSE);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION sys_insert_project_healths() RETURNS VOID AS
+$$
+BEGIN
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Not Set', '#a9a9a9', 0, TRUE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Needs Attention', '#fbc84c', 1, FALSE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('At Risk', '#f37070', 2, FALSE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Good', '#75c997', 3, FALSE);
+END;
+$$ LANGUAGE plpgsql;
+
+
+SELECT sys_insert_task_priorities();
+SELECT sys_insert_project_access_levels();
+SELECT sys_insert_task_status_categories();
+SELECT sys_insert_project_statuses();
+SELECT sys_insert_project_healths();
+
+DROP FUNCTION sys_insert_task_priorities();
+DROP FUNCTION sys_insert_project_access_levels();
+DROP FUNCTION sys_insert_task_status_categories();
+DROP FUNCTION sys_insert_project_statuses();
+DROP FUNCTION sys_insert_project_healths();
+
+INSERT INTO timezones (name, abbrev, utc_offset)
+SELECT name, abbrev, utc_offset
+FROM pg_timezone_names;
diff --git a/worklenz-backend/database/4_views.sql b/worklenz-backend/database/4_views.sql
new file mode 100644
index 00000000..78a934f7
--- /dev/null
+++ b/worklenz-backend/database/4_views.sql
@@ -0,0 +1,34 @@
+CREATE VIEW task_labels_view(name, task_id, label_id) AS
+SELECT (SELECT team_labels.name
+ FROM team_labels
+ WHERE team_labels.id = task_labels.label_id) AS name,
+ task_labels.task_id,
+ task_labels.label_id
+FROM task_labels;
+
+CREATE VIEW tasks_with_status_view(task_id, parent_task_id, is_todo, is_doing, is_done) AS
+SELECT tasks.id AS task_id,
+ tasks.parent_task_id,
+ stsc.is_todo,
+ stsc.is_doing,
+ stsc.is_done
+FROM tasks
+ JOIN task_statuses ts ON tasks.status_id = ts.id
+ JOIN sys_task_status_categories stsc ON ts.category_id = stsc.id
+WHERE tasks.archived IS FALSE;
+
+CREATE VIEW team_member_info_view(avatar_url, email, name, user_id, team_member_id, team_id) AS
+SELECT u.avatar_url,
+ COALESCE(u.email, (SELECT email_invitations.email
+ FROM email_invitations
+ WHERE email_invitations.team_member_id = team_members.id)) AS email,
+ COALESCE(u.name, (SELECT email_invitations.name
+ FROM email_invitations
+ WHERE email_invitations.team_member_id = team_members.id)) AS name,
+ u.id AS user_id,
+ team_members.id AS team_member_id,
+ team_members.team_id
+FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id;
+
+
diff --git a/worklenz-backend/database/5_functions.sql b/worklenz-backend/database/5_functions.sql
new file mode 100644
index 00000000..3ee7e10a
--- /dev/null
+++ b/worklenz-backend/database/5_functions.sql
@@ -0,0 +1,5791 @@
+CREATE OR REPLACE FUNCTION accept_invitation(_email text, _team_member_id uuid, _user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF _team_member_id IS NOT NULL
+ THEN
+ UPDATE team_members SET user_id = _user_id WHERE id = _team_member_id;
+ DELETE FROM email_invitations WHERE email = _email AND team_member_id = _team_member_id;
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'email', _email,
+ 'id', (SELECT id FROM teams WHERE id = (SELECT team_id FROM team_members WHERE id = _team_member_id))
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION activate_team(_team_id uuid, _user_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ UPDATE users
+ SET active_team =_team_id
+ WHERE id = _user_id
+ AND EXISTS(SELECT id FROM team_members WHERE team_id = _team_id AND user_id = _user_id);
+
+ DELETE
+ FROM email_invitations
+ WHERE team_id = _team_id
+ AND team_member_id =
+ (SELECT id FROM team_members WHERE user_id = _user_id AND team_members.team_id = _team_id);
+END
+$$;
+
+CREATE OR REPLACE FUNCTION add_or_remove_pt_task_label(_task_id uuid, _label_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ IF EXISTS(SELECT task_id FROM cpt_task_labels WHERE task_id = _task_id AND label_id = _label_id)
+ THEN
+ DELETE FROM cpt_task_labels WHERE task_id = _task_id AND label_id = _label_id;
+ ELSE
+ INSERT INTO cpt_task_labels (task_id, label_id) VALUES (_task_id, _label_id);
+ END IF;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (SELECT cpt_task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = cpt_task_labels.label_id) AS name,
+ (SELECT color_code FROM team_labels WHERE id = cpt_task_labels.label_id)
+ FROM cpt_task_labels
+ WHERE task_id = _task_id
+ ORDER BY name) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION add_or_remove_task_label(_task_id uuid, _label_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ IF EXISTS(SELECT task_id FROM task_labels WHERE task_id = _task_id AND label_id = _label_id)
+ THEN
+ DELETE FROM task_labels WHERE task_id = _task_id AND label_id = _label_id;
+ ELSE
+ INSERT INTO task_labels (task_id, label_id) VALUES (_task_id, _label_id);
+ END IF;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (SELECT task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = task_labels.label_id) AS name,
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = _task_id
+ ORDER BY name) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION assign_or_create_label(_team_id uuid, _task_id uuid, _name text, _color_code text) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _label_id UUID;
+ _is_new BOOLEAN;
+BEGIN
+ SELECT id FROM team_labels WHERE team_id = _team_id AND LOWER(name) = TRIM(LOWER(_name)) INTO _label_id;
+
+ IF (is_null_or_empty(_label_id) IS TRUE)
+ THEN
+ INSERT INTO team_labels (name, team_id, color_code)
+ VALUES (TRIM(_name), _team_id, _color_code)
+ RETURNING id INTO _label_id;
+ _is_new = TRUE;
+ END IF;
+
+ INSERT INTO task_labels (task_id, label_id) VALUES (_task_id, _label_id);
+
+ RETURN JSON_BUILD_OBJECT('id', _label_id, 'is_new', _is_new);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION assign_or_create_pt_label(_team_id uuid, _task_id uuid, _name text, _color_code text) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _label_id UUID;
+ _is_new BOOLEAN;
+BEGIN
+ SELECT id FROM team_labels WHERE team_id = _team_id AND LOWER(name) = TRIM(LOWER(_name)) INTO _label_id;
+
+ IF (is_null_or_empty(_label_id) IS TRUE)
+ THEN
+ INSERT INTO team_labels (name, team_id, color_code)
+ VALUES (TRIM(_name), _team_id, _color_code)
+ RETURNING id INTO _label_id;
+ _is_new = TRUE;
+ END IF;
+
+ INSERT INTO cpt_task_labels (task_id, label_id) VALUES (_task_id, _label_id);
+
+ RETURN JSON_BUILD_OBJECT('id', _label_id, 'is_new', _is_new);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_archive_tasks(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ -- Archive the parent task
+ UPDATE tasks
+ SET archived = ((_body ->> 'type')::TEXT = 'archive')
+ WHERE id = (_task ->> 'id')::UUID
+ AND parent_task_id IS NULL;
+ -- Prevent archiving subtasks
+
+ -- Archive its sub-tasks
+ UPDATE tasks
+ SET archived = ((_body ->> 'type')::TEXT = 'archive')
+ WHERE parent_task_id = (_task ->> 'id')::UUID;
+ END LOOP;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_assign_or_create_label(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ PERFORM assign_or_create_label((_body ->> 'team_id')::UUID, (_task ->> 'id')::UUID,
+ (_body ->> 'text')::TEXT, (_body ->> 'color')::TEXT);
+ END LOOP;
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_assign_to_me(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+ _project_member JSON;
+ _team_member_id UUID;
+ _project_member_id UUID;
+BEGIN
+ SELECT id
+ FROM team_members
+ WHERE team_id = (_body ->> 'team_id')::UUID
+ AND user_id = (_body ->> 'user_id')::UUID
+ INTO _team_member_id;
+
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+
+ SELECT id
+ FROM project_members
+ WHERE project_id = (_body ->> 'project_id')::UUID
+ AND project_members.team_member_id = _team_member_id
+ INTO _project_member_id;
+
+ IF is_null_or_empty(_project_member_id)
+ THEN
+ SELECT create_project_member(JSON_BUILD_OBJECT(
+ 'team_member_id', _team_member_id,
+ 'team_id', (_body ->> 'team_id')::UUID,
+ 'project_id', (_body ->> 'project_id')::UUID,
+ 'user_id', (_body ->> 'user_id')::UUID,
+ 'access_level', 'MEMBER'::TEXT
+ ))
+ INTO _project_member;
+ _project_member_id = (_project_member ->> 'id')::UUID;
+ END IF;
+
+ IF NOT EXISTS(SELECT task_id
+ FROM tasks_assignees
+ WHERE task_id = (_task ->> 'id')::UUID
+ AND project_member_id = _project_member_id
+ AND team_member_id = _team_member_id)
+ THEN
+ INSERT INTO tasks_assignees (task_id, project_member_id, team_member_id, assigned_by)
+ VALUES ((_task ->> 'id')::UUID, _project_member_id, _team_member_id, (_body ->> 'user_id')::UUID);
+ END IF;
+ END LOOP;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_delete_pt_tasks(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ DELETE FROM cpt_tasks WHERE id = (_task ->> 'id')::UUID;
+ END LOOP;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_delete_tasks(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ DELETE FROM tasks WHERE id = (_task ->> 'id')::UUID;
+ END LOOP;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION complete_account_setup(_user_id uuid, _team_id uuid, _body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_id UUID;
+ _default_status_id UUID;
+ _task_id UUID;
+ _task TEXT;
+ _members JSON;
+ _sort_order INT;
+ _team_member_id UUID;
+ _project_member_id UUID;
+BEGIN
+
+ -- Update team name
+ UPDATE teams SET name = TRIM((_body ->> 'team_name')::TEXT) WHERE id = _team_id AND user_id = _user_id;
+
+ -- Create the project
+ INSERT INTO projects (name, team_id, owner_id, color_code, status_id, key)
+ VALUES ((_body ->> 'project_name')::TEXT, _team_id, _user_id, '#3b7ad4',
+ (SELECT id FROM sys_project_statuses WHERE is_default IS TRUE), (_body ->> 'key')::TEXT)
+ RETURNING id INTO _project_id;
+
+ -- Insert task's statuses
+ INSERT INTO task_statuses (name, project_id, team_id, category_id)
+ VALUES ('To do', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))
+ RETURNING id INTO _default_status_id;
+
+ INSERT INTO task_statuses (name, project_id, team_id, category_id)
+ VALUES ('Doing', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE));
+
+ INSERT INTO task_statuses (name, project_id, team_id, category_id)
+ VALUES ('Done', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE));
+
+ SELECT id FROM team_members WHERE user_id = _user_id AND team_id = _team_id INTO _team_member_id;
+
+ INSERT INTO project_members (team_member_id, project_access_level_id, project_id, role_id)
+ VALUES (_team_member_id, (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'),
+ _project_id,
+ (SELECT id FROM roles WHERE team_id = _team_id AND default_role IS TRUE))
+ RETURNING id INTO _project_member_id;
+
+ -- Insert tasks
+ _sort_order = 1;
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ INSERT INTO tasks (name, priority_id, project_id, reporter_id, status_id, sort_order)
+ VALUES (TRIM('"' FROM _task)::TEXT, (SELECT id FROM task_priorities WHERE value = 1), _project_id, _user_id,
+ _default_status_id, _sort_order)
+ RETURNING id INTO _task_id;
+ _sort_order = _sort_order + 1;
+
+ INSERT INTO tasks_assignees (task_id, project_member_id, team_member_id, assigned_by)
+ VALUES (_task_id, _project_member_id, _team_member_id, _user_id);
+ END LOOP;
+
+ -- Insert team members if available
+ IF is_null_or_empty((_body ->> 'team_members')) IS FALSE
+ THEN
+ SELECT create_team_member(JSON_BUILD_OBJECT('team_id', _team_id, 'emails', (_body ->> 'team_members')))
+ INTO _members;
+ END IF;
+
+ -- insert default columns for task list
+ PERFORM insert_task_list_columns(_project_id);
+
+ UPDATE users SET setup_completed = TRUE WHERE id = _user_id;
+
+ -- Update organization name
+ UPDATE organizations SET organization_name = TRIM((_body ->> 'team_name')::TEXT) WHERE user_id = _user_id;
+
+ --insert user data
+ INSERT INTO users_data (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
+ trial_expire_date, subscription_status)
+ VALUES (_user_id, TRIM((_body ->> 'team_name')::TEXT), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '14 days',
+ 'trialing')
+ ON CONFLICT (user_id) DO UPDATE SET organization_name = TRIM((_body ->> 'team_name')::TEXT);
+
+ RETURN JSON_BUILD_OBJECT('id', _project_id, 'members', _members);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_bulk_task_assignees(_team_member_id uuid, _project_id uuid, _task_id uuid, _reporter_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_member JSON;
+ _project_member_id UUID;
+ _team_id UUID;
+ _user_id UUID;
+BEGIN
+ SELECT id
+ FROM project_members
+ WHERE team_member_id = _team_member_id
+ AND project_id = _project_id
+ INTO _project_member_id;
+
+ SELECT team_id FROM team_members WHERE id = _team_member_id INTO _team_id;
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _user_id;
+
+ IF is_null_or_empty(_project_member_id)
+ THEN
+ SELECT create_project_member(JSON_BUILD_OBJECT(
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'project_id', _project_id,
+ 'user_id', _reporter_user_id,
+ 'access_level', 'MEMBER'::TEXT
+ ))
+ INTO _project_member;
+ _project_member_id = (_project_member ->> 'id')::UUID;
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM tasks_assignees WHERE task_id = _task_id AND project_member_id = _project_member_id)
+ THEN
+ INSERT INTO tasks_assignees (task_id, project_member_id, team_member_id, assigned_by)
+ VALUES (_task_id, _project_member_id, _team_member_id, _reporter_user_id);
+
+ INSERT INTO task_activity_logs (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, project_id)
+ VALUES (
+ _task_id,
+ (SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = _task_id)),
+ 'assignee',
+ _reporter_user_id,
+ 'assign',
+ NULL,
+ _user_id,
+ (SELECT project_id FROM tasks WHERE id = _task_id)
+ );
+
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'task_id', _task_id,
+ 'project_member_id', _project_member_id,
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'user_id', _user_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_home_task(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task_id UUID;
+BEGIN
+
+ INSERT INTO tasks (name, end_date, priority_id, project_id, reporter_id, status_id, sort_order)
+ VALUES (TRIM((_body ->> 'name')::TEXT),
+ (_body ->> 'end_date')::TIMESTAMP,
+ (SELECT id FROM task_priorities WHERE value = 1),
+ (_body ->> 'project_id')::UUID,
+ (_body ->> 'reporter_id')::UUID,
+
+ -- This should be came from client side later
+ (SELECT id
+ FROM task_statuses
+ WHERE project_id = (_body ->> 'project_id')::UUID
+ AND category_id IN (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE)
+ LIMIT 1),
+ COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = (_body ->> 'project_id')::UUID), 0))
+ RETURNING id INTO _task_id;
+
+ -- RETURN(SELECT id FROM team_members WHERE user_id=(_body ->> 'reporter_id')::UUID AND team_id=(_body ->> 'team_id')::UUID);
+
+ RETURN home_task_form_view_model((_body ->> 'reporter_id')::UUID, (_body ->> 'team_id')::UUID, _task_id,
+ (_body ->> 'project_id')::UUID);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_new_team(_name text, _user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _owner_id UUID;
+ _team_id UUID;
+ _admin_role_id UUID;
+ _owner_role_id UUID;
+ _trimmed_name TEXT;
+ _trimmed_team_name TEXT;
+BEGIN
+
+ _trimmed_team_name = TRIM(_name);
+ -- get owner id
+ SELECT user_id INTO _owner_id FROM teams WHERE id = (SELECT active_team FROM users WHERE id = _user_id);
+
+ -- insert team
+ INSERT INTO teams (name, user_id)
+ VALUES (_trimmed_team_name, _owner_id)
+ RETURNING id INTO _team_id;
+
+ -- insert default roles
+ INSERT INTO roles (name, team_id, default_role) VALUES ('Member', _team_id, TRUE);
+ INSERT INTO roles (name, team_id, admin_role) VALUES ('Admin', _team_id, TRUE) RETURNING id INTO _admin_role_id;
+ INSERT INTO roles (name, team_id, owner) VALUES ('Owner', _team_id, TRUE) RETURNING id INTO _owner_role_id;
+
+ -- insert team member
+ INSERT INTO team_members (user_id, team_id, role_id)
+ VALUES (_owner_id, _team_id, _owner_role_id);
+
+ IF (_user_id <> _owner_id)
+ THEN
+ INSERT INTO team_members (user_id, team_id, role_id)
+ VALUES (_user_id, _team_id, _admin_role_id);
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _user_id,
+ 'name', _trimmed_name,
+ 'team_id', _team_id
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_notification(_user_id uuid, _team_id uuid, _task_id uuid, _project_id uuid, _message text) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF (_user_id IS NOT NULL AND _team_id IS NOT NULL AND is_null_or_empty(_message) IS FALSE)
+ THEN
+ INSERT INTO user_notifications (message, user_id, team_id, task_id, project_id)
+ VALUES (TRIM(_message), _user_id, _team_id, _task_id, _project_id);
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'project', (SELECT name FROM projects WHERE id = _project_id),
+ 'project_color', (SELECT color_code FROM projects WHERE id = _project_id),
+ 'team', (SELECT name FROM teams WHERE id = _team_id)
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_project(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _user_id UUID;
+ _team_id UUID;
+ _client_id UUID;
+ _project_id UUID;
+ _client_name TEXT;
+ _project_name TEXT;
+BEGIN
+ -- need a test, can be throw errors
+ _client_name = TRIM((_body ->> 'client_name')::TEXT);
+ _project_name = TRIM((_body ->> 'name')::TEXT);
+
+ -- add inside the controller
+ _user_id = (_body ->> 'user_id')::UUID;
+ _team_id = (_body ->> 'team_id')::UUID;
+
+ -- cache exists client if exists
+ SELECT id FROM clients WHERE LOWER(name) = LOWER(_client_name) AND team_id = _team_id INTO _client_id;
+
+ -- check whether the project name is already in
+ IF EXISTS(SELECT name
+ FROM projects
+ WHERE LOWER(name) = LOWER(_project_name)
+ AND team_id = _team_id)
+ THEN
+ RAISE 'PROJECT_EXISTS_ERROR:%', _project_name;
+ END IF;
+
+ -- insert client if not exists
+ IF is_null_or_empty(_client_id) IS TRUE AND is_null_or_empty(_client_name) IS FALSE
+ THEN
+ INSERT INTO clients (name, team_id) VALUES (_client_name, _team_id) RETURNING id INTO _client_id;
+ END IF;
+
+ -- insert project
+ INSERT INTO projects (name, key, notes, color_code, team_id, client_id, owner_id, status_id, health_id, start_date,
+ end_date,
+ folder_id, category_id, estimated_working_days, estimated_man_days, hours_per_day)
+ VALUES (_project_name, (_body ->> 'key')::TEXT, (_body ->> 'notes')::TEXT, (_body ->> 'color_code')::TEXT, _team_id,
+ _client_id,
+ _user_id, (_body ->> 'status_id')::UUID, (_body ->> 'health_id')::UUID,
+ (_body ->> 'start_date')::TIMESTAMPTZ,
+ (_body ->> 'end_date')::TIMESTAMPTZ, (_body ->> 'folder_id')::UUID, (_body ->> 'category_id')::UUID,
+ (_body ->> 'working_days')::INTEGER, (_body ->> 'man_days')::INTEGER, (_body ->> 'hours_per_day')::INTEGER)
+ RETURNING id INTO _project_id;
+
+ -- log record
+ INSERT INTO project_logs (team_id, project_id, description)
+ VALUES (_team_id, _project_id,
+ REPLACE((_body ->> 'project_created_log')::TEXT, '@user',
+ (SELECT name FROM users WHERE id = _user_id)));
+
+ -- insert statuses
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('To Do', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE), 0);
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('Doing', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE), 1);
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES ('Done', _project_id, _team_id, (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE), 2);
+
+ -- insert default columns for task list
+ PERFORM insert_task_list_columns(_project_id);
+
+ -- insert user as default project manager(project owner)
+ IF NOT (_body ->> 'project_manager_id' IS NULL)
+ THEN
+ PERFORM create_project_member(JSON_BUILD_OBJECT(
+ 'team_member_id', (_body ->> 'project_manager_id')::UUID,
+ 'team_id', _team_id,
+ 'project_id', _project_id,
+ 'user_id', _user_id,
+ 'access_level', 'PROJECT_MANAGER'::TEXT
+ ));
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _project_id,
+ 'name', (_body ->> 'name')::TEXT
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_project_comment(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_id UUID;
+ _created_by UUID;
+ _comment_id UUID;
+ _team_id UUID;
+ _user_name TEXT;
+ _project_name TEXT;
+ _content TEXT;
+ _mention_index INT := 0;
+ _mention JSON;
+BEGIN
+ _project_id = (_body ->> 'project_id');
+ _created_by = (_body ->> 'created_by');
+ _content = (_body ->> 'content');
+ _team_id = (_body ->> 'team_id');
+
+ SELECT name FROM users WHERE id = _created_by LIMIT 1 INTO _user_name;
+ SELECT name FROM projects WHERE id = _project_id INTO _project_name;
+
+ INSERT INTO project_comments (content, created_by, project_id)
+ VALUES (_content, _created_by, _project_id)
+ RETURNING id INTO _comment_id;
+
+ FOR _mention IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'mentions')::JSON)
+ LOOP
+
+ INSERT INTO project_comment_mentions (comment_id, mentioned_index, mentioned_by, informed_by)
+ VALUES (_comment_id, _mention_index, _created_by, (_mention ->> 'id')::UUID);
+
+ PERFORM create_notification(
+ (SELECT id FROM users WHERE id = (_mention ->> 'id')::UUID),
+ (_team_id)::UUID,
+ null,
+ (_project_id)::UUID,
+ CONCAT('', _user_name, ' has mentioned you in a comment on ', _project_name, '')
+ );
+ _mention_index := _mention_index + 1;
+
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', (_comment_id)::UUID,
+ 'content', (_content)::TEXT,
+ 'project_name', (_project_name)::TEXT,
+ 'team_name', (SELECT name FROM teams WHERE id = (_team_id)::UUID)
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_project_member(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _id UUID;
+ _team_member_id UUID;
+ _team_id UUID;
+ _project_id UUID;
+ _user_id UUID;
+ _member_user_id UUID;
+ _notification TEXT;
+ _access_level TEXT;
+BEGIN
+ _team_member_id = (_body ->> 'team_member_id')::UUID;
+ _team_id = (_body ->> 'team_id')::UUID;
+ _project_id = (_body ->> 'project_id')::UUID;
+ _user_id = (_body ->> 'user_id')::UUID;
+ _access_level = (_body ->> 'access_level')::TEXT;
+
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _member_user_id;
+
+ INSERT INTO project_members (team_member_id, project_access_level_id, project_id, role_id)
+ VALUES (_team_member_id, (SELECT id FROM project_access_levels WHERE key = _access_level)::UUID,
+ _project_id,
+ (SELECT id FROM roles WHERE team_id = _team_id AND default_role IS TRUE))
+ RETURNING id INTO _id;
+
+ IF (_member_user_id != _user_id)
+ THEN
+ _notification = CONCAT('You have been added to the ',
+ (SELECT name FROM projects WHERE id = _project_id),
+ ' by ',
+ (SELECT name FROM users WHERE id = _user_id), '');
+ PERFORM create_notification(
+ (SELECT user_id FROM team_members WHERE id = _team_member_id),
+ _team_id,
+ NULL,
+ _project_id,
+ _notification
+ );
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _id,
+ 'notification', _notification,
+ 'socket_id', (SELECT socket_id FROM users WHERE id = _member_user_id),
+ 'project', (SELECT name FROM projects WHERE id = _project_id),
+ 'project_id', _project_id,
+ 'project_color', (SELECT color_code FROM projects WHERE id = _project_id),
+ 'team', (SELECT name FROM teams WHERE id = _team_id),
+ 'member_user_id', _member_user_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_project_template(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _template_id UUID;
+BEGIN
+ -- check whether the project name is already in
+ IF EXISTS(SELECT name
+ FROM custom_project_templates
+ WHERE LOWER(name) = LOWER((_body ->> 'name')::TEXT)
+ AND team_id = (_body ->> 'team_id')::uuid)
+ THEN
+ RAISE 'TEMPLATE_EXISTS_ERROR:%', (_body ->> 'name')::TEXT;
+ END IF;
+
+ -- insert client if not exists
+ INSERT INTO custom_project_templates(name, phase_label, color_code, notes, team_id)
+ VALUES ((_body ->> 'name')::TEXT, (_body ->> 'phase_label')::TEXT, (_body ->> 'color_code')::TEXT,
+ (_body ->> 'notes')::TEXT,
+ (_body ->> 'team_id')::uuid)
+ RETURNING id INTO _template_id;
+
+ RETURN JSON_BUILD_OBJECT('id', _template_id);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_pt_task_status(_body json, _team_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _status_id UUID;
+ _group_status JSON;
+BEGIN
+ INSERT INTO cpt_task_statuses (name, template_id, team_id, category_id, sort_order)
+ VALUES (TRIM((_body ->> 'name')::TEXT),
+ (_body ->> 'template_id')::UUID,
+ _team_id,
+ (_body ->> 'category_id')::UUID,
+ COALESCE((SELECT MAX(sort_order) + 1
+ FROM cpt_task_statuses
+ WHERE template_id = (_body ->> 'template_id') ::UUID),
+ 0)) RETURNING id INTO _status_id;
+ SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec)))
+ FROM (SELECT id,
+ name,
+ template_id,
+ team_id,
+ category_id,
+ sort_order,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM cpt_task_statuses WHERE id = _status_id)) AS color_code
+ FROM cpt_task_statuses
+ WHERE id = _status_id) rec INTO _group_status;
+ RETURN _group_status;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_quick_pt_task(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task_id UUID;
+ _parent_task UUID;
+ _status_id UUID;
+ _priority_id UUID;
+BEGIN
+
+ _parent_task = (_body ->> 'parent_task_id')::UUID;
+ _status_id = COALESCE(
+ (_body ->> 'status_id')::UUID,
+ (SELECT id
+ FROM cpt_task_statuses
+ WHERE template_id = (_body ->> 'template_id')::UUID
+ AND category_id IN (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE)
+ LIMIT 1)
+ );
+ _priority_id = COALESCE((_body ->> 'priority_id')::UUID, (SELECT id FROM task_priorities WHERE value = 1));
+
+ INSERT INTO cpt_tasks(name, priority_id, template_id, status_id, parent_task_id, sort_order, task_no)
+ VALUES (TRIM((_body ->> 'name')::TEXT),
+ _priority_id,
+ (_body ->> 'template_id')::UUID,
+
+ -- This should be came from client side later
+ _status_id, _parent_task,
+ COALESCE((SELECT MAX(sort_order) + 1 FROM cpt_tasks WHERE template_id = (_body ->> 'template_id')::UUID),
+ 0), ((SELECT COUNT(*) FROM cpt_tasks WHERE template_id = (_body ->> 'template_id')::UUID) + 1))
+ RETURNING id INTO _task_id;
+
+ PERFORM handle_on_pt_task_phase_change(_task_id, (_body ->> 'phase_id')::UUID);
+
+ RETURN get_single_pt_task(_task_id);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_quick_task(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task_id UUID;
+ _parent_task UUID;
+ _status_id UUID;
+ _priority_id UUID;
+ _start_date TIMESTAMP;
+ _end_date TIMESTAMP;
+BEGIN
+
+ _parent_task = (_body ->> 'parent_task_id')::UUID;
+ _status_id = COALESCE(
+ (_body ->> 'status_id')::UUID,
+ (SELECT id
+ FROM task_statuses
+ WHERE project_id = (_body ->> 'project_id')::UUID
+ AND category_id IN (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE)
+ LIMIT 1)
+ );
+ _priority_id = COALESCE((_body ->> 'priority_id')::UUID, (SELECT id FROM task_priorities WHERE value = 1));
+ _start_date = (_body ->> 'start_date')::TIMESTAMP;
+ _end_date = (_body ->> 'end_date')::TIMESTAMP;
+
+ INSERT INTO tasks (name, priority_id, project_id, reporter_id, status_id, parent_task_id, sort_order, start_date, end_date)
+ VALUES (TRIM((_body ->> 'name')::TEXT),
+ _priority_id,
+ (_body ->> 'project_id')::UUID,
+ (_body ->> 'reporter_id')::UUID,
+
+ -- This should be came from client side later
+ _status_id, _parent_task,
+ COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = (_body ->> 'project_id')::UUID), 0),
+ (_body ->> 'start_date')::TIMESTAMP,
+ (_body ->> 'end_date')::TIMESTAMP)
+ RETURNING id INTO _task_id;
+
+ PERFORM handle_on_task_phase_change(_task_id, (_body ->> 'phase_id')::UUID);
+
+ RETURN get_single_task(_task_id);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_task(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _assignee TEXT;
+ _attachment_id TEXT;
+ _assignee_id UUID;
+ _task_id UUID;
+ _label JSON;
+BEGIN
+ INSERT INTO tasks (name, done, priority_id, project_id, reporter_id, start_date, end_date, total_minutes,
+ description, parent_task_id, status_id, sort_order)
+ VALUES (TRIM((_body ->> 'name')::TEXT), (FALSE),
+ COALESCE((_body ->> 'priority_id')::UUID, (SELECT id FROM task_priorities WHERE value = 1)),
+ (_body ->> 'project_id')::UUID,
+ (_body ->> 'reporter_id')::UUID,
+ (_body ->> 'start')::TIMESTAMPTZ,
+ (_body ->> 'end')::TIMESTAMPTZ,
+ (_body ->> 'total_minutes')::NUMERIC,
+ (_body ->> 'description')::TEXT,
+ (_body ->> 'parent_task_id')::UUID,
+ (_body ->> 'status_id')::UUID,
+ COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = (_body ->> 'project_id')::UUID), 0))
+ RETURNING id INTO _task_id;
+
+ -- insert task assignees
+ FOR _assignee IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'assignees')::JSON)
+ LOOP
+ _assignee_id = TRIM('"' FROM _assignee)::UUID;
+ PERFORM create_task_assignee(_assignee_id, (_body ->> 'project_id')::UUID, _task_id,
+ (_body ->> 'reporter_id')::UUID);
+ END LOOP;
+
+ FOR _attachment_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'attachments')::JSON)
+ LOOP
+ UPDATE task_attachments SET task_id = _task_id WHERE id = TRIM('"' FROM _attachment_id)::UUID;
+ END LOOP;
+
+ FOR _label IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'labels')::JSON)
+ LOOP
+ PERFORM assign_or_create_label((_body ->> 'team_id')::UUID, _task_id, (_label ->> 'name')::TEXT,
+ (_label ->> 'color')::TEXT);
+ END LOOP;
+
+ RETURN get_task_form_view_model((_body ->> 'reporter_id')::UUID, (_body ->> 'team_id')::UUID, _task_id,
+ (_body ->> 'project_id')::UUID);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_task_assignee(_team_member_id uuid, _project_id uuid, _task_id uuid, _reporter_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_member JSON;
+ _project_member_id UUID;
+ _team_id UUID;
+ _user_id UUID;
+BEGIN
+ SELECT id
+ FROM project_members
+ WHERE team_member_id = _team_member_id
+ AND project_id = _project_id
+ INTO _project_member_id;
+
+ SELECT team_id FROM team_members WHERE id = _team_member_id INTO _team_id;
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _user_id;
+
+ IF is_null_or_empty(_project_member_id)
+ THEN
+ SELECT create_project_member(JSON_BUILD_OBJECT(
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'project_id', _project_id,
+ 'user_id', _reporter_user_id,
+ 'access_level', 'MEMBER'::TEXT
+ ))
+ INTO _project_member;
+ _project_member_id = (_project_member ->> 'id')::UUID;
+ END IF;
+
+ INSERT INTO tasks_assignees (task_id, project_member_id, team_member_id, assigned_by)
+ VALUES (_task_id, _project_member_id, _team_member_id, _reporter_user_id);
+
+ RETURN JSON_BUILD_OBJECT(
+ 'task_id', _task_id,
+ 'project_member_id', _project_member_id,
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'user_id', _user_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_task_comment(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task_id UUID;
+ _user_id UUID;
+ _comment_id UUID;
+ _team_member_id UUID;
+ _mentioned_member_id TEXT;
+ _user_name TEXT;
+ _task_name TEXT;
+ _mention_index INT := 0;
+ _mention JSON;
+BEGIN
+
+ _task_id = (_body ->> 'task_id')::UUID;
+ _user_id = (_body ->> 'user_id')::UUID;
+
+ SELECT name FROM users WHERE id = _user_id LIMIT 1 INTO _user_name;
+ SELECT name FROM tasks WHERE id = _task_id INTO _task_name;
+
+ SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = (_body ->> 'team_id')::UUID
+ INTO _team_member_id;
+
+ INSERT INTO task_comments (user_id, team_member_id, task_id)
+ VALUES (_user_id, _team_member_id, _task_id)
+ RETURNING id INTO _comment_id;
+
+ INSERT INTO task_comment_contents (index, comment_id, text_content)
+ VALUES (0, _comment_id, (_body ->> 'content')::TEXT);
+
+ -- notify mentions
+ FOR _mention IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'mentions')::JSON)
+ LOOP
+ INSERT INTO task_comment_mentions (comment_id, mentioned_index, mentioned_by, informed_by)
+ VALUES (_comment_id, _mention_index, _user_id, (_mention ->> 'team_member_id')::UUID);
+ PERFORM create_notification(
+ (SELECT user_id FROM team_members WHERE id = TRIM(BOTH '"' FROM (_mention ->> 'team_member_id'))::UUID),
+ (_body ->> 'team_id')::UUID,
+ _task_id,
+ (SELECT project_id FROM tasks WHERE id = _task_id),
+ CONCAT('', _user_name, ' has mentioned you in a comment on ', _task_name, '')
+ );
+ _mention_index := _mention_index + 1;
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _comment_id,
+ 'content', (_body ->> 'content')::TEXT,
+ 'task_name', _task_name,
+ 'project_id', (SELECT project_id FROM tasks WHERE id = _task_id),
+ 'project_name', (SELECT name FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = _task_id)),
+ 'team_name', (SELECT name FROM teams WHERE id = (_body ->> 'team_id')::UUID)
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_task_status(_body json, _team_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _status_id UUID;
+ _group_status JSON;
+BEGIN
+
+ INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order)
+ VALUES (TRIM((_body ->> 'name')::TEXT),
+ (_body ->> 'project_id')::UUID,
+ _team_id,
+ (_body ->> 'category_id')::UUID,
+ COALESCE((SELECT MAX(sort_order) + 1 FROM task_statuses WHERE project_id = (_body ->> 'project_id')::UUID),
+ 0))
+ RETURNING id INTO _status_id;
+
+ SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec)))
+ FROM (SELECT id,
+ name,
+ project_id,
+ team_id,
+ category_id,
+ sort_order,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = _status_id)) AS color_code
+ FROM task_statuses
+ WHERE id = _status_id) rec
+ INTO _group_status;
+
+ RETURN _group_status;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION create_task_template(_name text, _team_id uuid, _tasks json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _template_id UUID;
+ _task JSON;
+BEGIN
+ INSERT INTO task_templates (name, team_id) VALUES (_name, _team_id) RETURNING id INTO _template_id;
+
+ -- insert tasks for task templates
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS(_tasks)
+ LOOP
+ INSERT INTO task_templates_tasks (template_id, name, total_minutes) VALUES (_template_id, (_task ->> 'name')::TEXT, (SELECT total_minutes FROM tasks WHERE id = (_task ->> 'id')::UUID)::NUMERIC);
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _template_id,
+ 'template_name', _name
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION create_team_member(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _team_id UUID;
+ _user_id UUID;
+ _job_title_id UUID;
+ _team_member_id UUID;
+ _role_id UUID;
+ _email TEXT;
+ _output JSON;
+BEGIN
+ _team_id = (_body ->> 'team_id')::UUID;
+
+ IF ((_body ->> 'is_admin')::BOOLEAN IS TRUE)
+ THEN
+ SELECT id FROM roles WHERE team_id = _team_id AND admin_role IS TRUE INTO _role_id;
+ ELSE
+ SELECT id FROM roles WHERE team_id = _team_id AND default_role IS TRUE INTO _role_id;
+ END IF;
+
+ IF is_null_or_empty((_body ->> 'job_title')) IS FALSE
+ THEN
+ SELECT insert_job_title((_body ->> 'job_title')::TEXT, _team_id) INTO _job_title_id;
+ ELSE
+ _job_title_id = NULL;
+ END IF;
+
+ CREATE TEMPORARY TABLE temp_new_team_members (
+ name TEXT,
+ email TEXT,
+ is_new BOOLEAN,
+ team_member_id UUID,
+ team_member_user_id UUID
+ );
+
+ FOR _email IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'emails')::JSON)
+ LOOP
+
+ _email = LOWER(TRIM('"' FROM _email)::TEXT);
+
+ SELECT id FROM users WHERE email = _email INTO _user_id;
+
+ INSERT INTO team_members (job_title_id, user_id, team_id, role_id)
+ VALUES (_job_title_id, _user_id, _team_id, _role_id)
+ RETURNING id INTO _team_member_id;
+
+ IF EXISTS(SELECT id
+ FROM email_invitations
+ WHERE email = _email
+ AND team_id = _team_id)
+ THEN
+ -- DELETE
+-- FROM team_members
+-- WHERE id = (SELECT team_member_id
+-- FROM email_invitations
+-- WHERE email = _email
+-- AND team_id = _team_id);
+-- DELETE FROM email_invitations WHERE team_id = _team_id AND email = _email;
+
+ DELETE FROM email_invitations WHERE email = _email AND team_id = _team_id;
+
+-- RAISE 'ERROR_EMAIL_INVITATION_EXISTS:%', _email;
+ END IF;
+
+ INSERT INTO email_invitations(team_id, team_member_id, email, name)
+ VALUES (_team_id, _team_member_id, _email, SPLIT_PART(_email, '@', 1));
+
+ INSERT INTO temp_new_team_members (is_new, team_member_id, team_member_user_id, name, email)
+ VALUES ((is_null_or_empty(_user_id)), _team_member_id, _user_id,
+ (SELECT name FROM users WHERE id = _user_id), _email);
+ END LOOP;
+
+ SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec)))
+ FROM (SELECT * FROM temp_new_team_members) rec
+ INTO _output;
+
+ DROP TABLE temp_new_team_members;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION delete_user(_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ SET SESSION_REPLICATION_ROLE = replica;
+
+ UPDATE users SET active_team = NULL WHERE id = _id;
+ DELETE FROM notification_settings WHERE user_id = _id;
+ DELETE FROM teams WHERE user_id = _id;
+ DELETE FROM roles WHERE team_id IN (SELECT id FROM teams WHERE user_id = _id);
+ DELETE
+ FROM tasks_assignees
+ WHERE project_member_id IN
+ (SELECT id FROM project_members WHERE team_member_id IN (SELECT id FROM team_members WHERE user_id = _id));
+ DELETE FROM project_members WHERE team_member_id IN (SELECT id FROM team_members WHERE user_id = _id);
+ DELETE FROM team_members WHERE user_id = _id;
+ DELETE FROM job_titles WHERE team_id = (SELECT id FROM teams WHERE user_id = _id);
+ DELETE
+ FROM tasks
+ WHERE project_id IN (SELECT id FROM projects WHERE team_id = (SELECT id FROM teams WHERE user_id = _id));
+ DELETE FROM projects WHERE team_id = (SELECT id FROM teams WHERE user_id = _id);
+ DELETE FROM clients WHERE team_id = (SELECT id FROM teams WHERE user_id = _id);
+ DELETE FROM teams WHERE user_id = _id;
+ DELETE FROM personal_todo_list WHERE user_id = _id;
+ DELETE FROM user_notifications WHERE user_id = _id;
+ UPDATE users SET active_team = NULL WHERE id = _id;
+ DELETE FROM users WHERE id = _id;
+
+ SET SESSION_REPLICATION_ROLE = default;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION deserialize_user(_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+ _team_id UUID;
+BEGIN
+
+ SELECT active_team FROM users WHERE id = _id INTO _team_id;
+ IF NOT EXISTS(SELECT 1 FROM notification_settings WHERE team_id = _team_id AND user_id = _id)
+ THEN
+ INSERT INTO notification_settings (popup_notifications_enabled, show_unread_items_count, user_id, team_id)
+ VALUES (TRUE, TRUE, _id, _team_id);
+ END IF;
+
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT users.id,
+ users.name,
+ users.email,
+ users.timezone_id AS timezone,
+ (SELECT name FROM timezones WHERE id = users.timezone_id) AS timezone_name,
+ users.avatar_url,
+ users.user_no,
+ users.socket_id,
+ users.created_at AS joined_date,
+ users.updated_at AS last_updated,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT description, type FROM worklenz_alerts WHERE active is TRUE) rec) AS alerts,
+
+ (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE user_id = users.id
+ AND team_id = t.id) AS email_notifications_enabled,
+ (CASE
+ WHEN is_owner(users.id, users.active_team) THEN users.setup_completed
+ ELSE TRUE END) AS setup_completed,
+ users.setup_completed AS my_setup_completed,
+ (is_null_or_empty(users.google_id) IS FALSE) AS is_google,
+ t.name AS team_name,
+ t.id AS team_id,
+ (SELECT id
+ FROM team_members
+ WHERE team_members.user_id = _id
+ AND team_id = users.active_team
+ AND active IS TRUE) AS team_member_id,
+ is_owner(users.id, users.active_team) AS owner,
+ is_admin(users.id, users.active_team) AS is_admin,
+ t.user_id AS owner_id,
+ ud.subscription_status
+ FROM users
+ INNER JOIN teams t
+ ON t.id = COALESCE(users.active_team,
+ (SELECT id FROM teams WHERE teams.user_id = users.id LIMIT 1))
+ LEFT JOIN organizations ud ON ud.user_id = t.user_id
+ WHERE users.id = _id) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_activity_logs_by_task(_task_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT (SELECT tasks.created_at FROM tasks WHERE tasks.id = _task_id),
+ (SELECT name
+ FROM users
+ WHERE id = (SELECT reporter_id FROM tasks WHERE id = _task_id)),
+ (SELECT avatar_url
+ FROM users
+ WHERE id = (SELECT reporter_id FROM tasks WHERE id = _task_id)),
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec2))), '[]'::JSON)
+ FROM (SELECT task_id,
+ created_at,
+ attribute_type,
+ log_type,
+
+ -- new case,
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
+ THEN (SELECT name FROM project_phases WHERE id = old_value::UUID)
+ ELSE (old_value) END) AS previous,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT name FROM users WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'label')
+ THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
+ THEN (SELECT name FROM project_phases WHERE id = new_value::UUID)
+ ELSE (new_value) END) AS current,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (CASE
+ WHEN (new_value IS NOT NULL)
+ THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
+ ELSE (next_string) END) AS name,
+ (SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS assigned_user,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'label')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM team_labels WHERE id = new_value::UUID),
+ (SELECT color_code FROM team_labels WHERE id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS label_data,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_statuses WHERE id = old_value::UUID),
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = old_value::UUID))) rec)
+ ELSE (NULL) END) AS previous_status,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_statuses WHERE id = new_value::UUID),
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = new_value::UUID))) rec)
+ ELSE (NULL) END) AS next_status,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_priorities WHERE id = old_value::UUID),
+ (SELECT color_code FROM task_priorities WHERE id = old_value::UUID)) rec)
+ ELSE (NULL) END) AS previous_priority,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_priorities WHERE id = new_value::UUID),
+ (SELECT color_code FROM task_priorities WHERE id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS next_priority,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM project_phases WHERE id = old_value::UUID),
+ (SELECT color_code FROM project_phases WHERE id = old_value::UUID)) rec)
+ ELSE (NULL) END) AS previous_phase,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM project_phases WHERE id = new_value::UUID),
+ (SELECT color_code FROM project_phases WHERE id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS next_phase,
+
+ -- new case
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
+ (SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by
+
+
+ FROM task_activity_logs tal
+ WHERE task_id = _task_id
+ ORDER BY created_at DESC) rec2) AS logs) rec;
+ RETURN _result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_daily_digest() RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (
+ --
+ SELECT name,
+ email,
+ (SELECT get_daily_digest_recently_assigned(u.id)) AS recently_assigned,
+ (SELECT get_daily_digest_overdue(u.id)) AS overdue,
+ (SELECT get_daily_digest_recently_completed(u.id)) AS recently_completed
+ FROM users u
+ --
+ ) rec;
+ RETURN _result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_daily_digest_overdue(_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (
+ --
+ SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS projects
+ FROM (
+ --
+ SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS tasks
+ FROM (
+ --
+ SELECT t.id,
+ t.name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = t.id) AS members
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN team_members tm ON tasks_assignees.team_member_id = tm.id
+ WHERE tm.user_id = _user_id
+ AND t.project_id = projects.id
+ AND t.end_date IS NOT NULL
+ AND t.end_date < CURRENT_DATE
+ AND EXISTS(SELECT id
+ FROM task_statuses
+ WHERE id = t.status_id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ LIMIT 10
+ --
+ ) r)
+ FROM projects
+ WHERE projects.team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_members.team_id = teams.id)
+ --
+ ) r)
+ FROM teams
+ WHERE (SELECT daily_digest_enabled
+ FROM notification_settings
+ WHERE team_id = teams.id
+ AND user_id = _user_id) IS TRUE
+ AND EXISTS(SELECT 1
+ FROM team_members
+ WHERE team_id = teams.id
+ AND team_members.user_id = _user_id)
+ --
+ ) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_daily_digest_recently_assigned(_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (
+ --
+
+ --
+ SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS projects
+ FROM (
+ --
+ SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS tasks
+ FROM (
+ --
+ SELECT t.id,
+ t.name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = t.id) AS members
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN team_members tm ON tasks_assignees.team_member_id = tm.id
+ WHERE tm.user_id = _user_id
+ AND t.project_id = projects.id
+ AND TO_CHAR(tasks_assignees.created_at, 'yyyy-mm-dd') =
+ TO_CHAR(CURRENT_DATE, 'yyyy-mm-dd')
+ --
+ ) r)
+ FROM projects
+ WHERE projects.team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_members.team_id = teams.id)
+ --
+ ) r)
+ FROM teams
+ WHERE (SELECT daily_digest_enabled
+ FROM notification_settings
+ WHERE team_id = teams.id
+ AND user_id = _user_id) IS TRUE
+ AND EXISTS(SELECT 1
+ FROM team_members
+ WHERE team_id = teams.id
+ AND team_members.user_id = _user_id)
+ --
+ ) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_daily_digest_recently_completed(_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (
+ --
+ SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS projects
+ FROM (
+ --
+ SELECT name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS tasks
+ FROM (
+ --
+ SELECT t.id,
+ t.name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = t.id) AS members
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN team_members tm ON tasks_assignees.team_member_id = tm.id
+ WHERE tm.user_id = _user_id
+ AND t.project_id = projects.id
+ AND t.completed_at IS NOT NULL
+ AND TO_CHAR(t.completed_at, 'yyyy-mm-dd') =
+ TO_CHAR(CURRENT_DATE, 'yyyy-mm-dd')
+ LIMIT 10
+ --
+ ) r)
+ FROM projects
+ WHERE projects.team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_members.team_id = teams.id)
+ --
+ ) r)
+ FROM teams
+ WHERE (SELECT daily_digest_enabled
+ FROM notification_settings
+ WHERE team_id = teams.id
+ AND user_id = _user_id) IS TRUE
+ AND EXISTS(SELECT 1
+ FROM team_members
+ WHERE team_id = teams.id
+ AND team_members.user_id = _user_id)
+ --
+ ) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_last_updated_tasks_by_project(_project_id uuid, _limit integer, _offset integer, _archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT id,
+ name,
+ (SELECT name FROM task_statuses WHERE status_id = task_statuses.id) AS status,
+ status_id,
+ end_date,
+ priority_id AS priority,
+ updated_at,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ ORDER BY updated_at DESC
+ LIMIT _limit OFFSET _offset) rec;
+ RETURN _tasks;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_my_tasks(_team_id uuid, _user_id uuid, _size numeric, _offset numeric, _filter text) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ -- _filter = '0' is tasks due today
+ IF _filter = '0'
+ THEN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT t.id,
+ t.name,
+ t.project_id,
+ t.status_id,
+ t.start_date,
+ t.end_date,
+ t.created_at,
+ p.team_id,
+ p.name AS project_name,
+ p.color_code AS project_color,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ TRUE AS is_task
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE = CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))
+ ORDER BY p.updated_at, created_at
+ LIMIT _size OFFSET _offset) rec) AS data
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE = CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))) rec;
+ RETURN _result;
+ END IF;
+
+ -- _filter = '1' is upcoming tasks
+ IF _filter = '1'
+ THEN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT t.id,
+ t.name,
+ t.project_id,
+ t.status_id,
+ t.start_date,
+ t.end_date,
+ t.created_at,
+ p.team_id,
+ p.name AS project_name,
+ p.color_code AS project_color,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ TRUE AS is_task
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE > CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))
+ ORDER BY p.updated_at, created_at
+ LIMIT _size OFFSET _offset) rec) AS data
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE > CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))) rec;
+ RETURN _result;
+ END IF;
+
+ -- _filter = '2' is overdue tasks
+ IF _filter = '2'
+ THEN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT t.id,
+ t.name,
+ t.project_id,
+ t.status_id,
+ t.start_date,
+ t.end_date,
+ t.created_at,
+ p.team_id,
+ p.name AS project_name,
+ p.color_code AS project_color,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ TRUE AS is_task
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE < CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))
+ ORDER BY p.updated_at, created_at
+ LIMIT _size OFFSET _offset) rec) AS data
+ FROM tasks t
+ CROSS JOIN projects p
+ WHERE t.project_id = p.id
+ AND t.archived IS FALSE
+ AND t.end_date::DATE < CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.id IN
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND team_id = _team_id))) rec;
+ RETURN _result;
+ END IF;
+
+ -- _filter = '3' is todo list
+ IF _filter = '3'
+ THEN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id, name, created_at, color_code, done
+ FROM personal_todo_list
+ WHERE user_id = _user_id
+ ORDER BY updated_at, created_at
+ LIMIT _size OFFSET _offset) rec) AS data) rec;
+ RETURN _result;
+ END IF;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_daily_digest() RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+
+ SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ INTO _result
+ FROM (SELECT id,
+ name,
+ (SELECT name FROM teams WHERE id = projects.team_id) AS team_name,
+
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = tasks.id) AS members
+ FROM tasks
+ WHERE project_id = projects.id
+ AND TO_CHAR(tasks.completed_at, 'yyyy-mm-dd') =
+ TO_CHAR(CURRENT_DATE, 'yyyy-mm-dd')) rec) AS today_completed,
+
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = tasks.id) AS members
+ FROM tasks
+ WHERE project_id = projects.id
+ AND TO_CHAR(tasks.created_at, 'yyyy-mm-dd') =
+ TO_CHAR(CURRENT_DATE, 'yyyy-mm-dd')) rec) AS today_new,
+
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = tasks.id) AS members
+ FROM tasks
+ WHERE project_id = projects.id
+ AND TO_CHAR(tasks.end_date, 'yyyy-mm-dd') =
+ TO_CHAR(CURRENT_DATE + INTERVAL '1 day', 'yyyy-mm-dd')) rec) AS due_tomorrow,
+
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT name, email
+ FROM users
+ WHERE id = (SELECT user_id
+ FROM project_subscribers
+ WHERE project_id = projects.id
+ AND user_id = users.id)) rec) AS subscribers
+
+ FROM projects
+ WHERE EXISTS(SELECT 1 FROM project_subscribers WHERE project_id = projects.id)
+ ORDER BY team_id, name) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_deadline_tasks(_project_id uuid, _archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM (SELECT (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND end_date::DATE > (SELECT end_date
+ FROM projects
+ WHERE id = _project_id)::DATE) AS deadline_tasks_count,
+ (SELECT SUM(twl.time_spent)
+ FROM tasks t
+ CROSS JOIN task_work_log twl
+ WHERE twl.task_id = t.id
+ AND t.project_id = _project_id
+ AND twl.created_at::DATE > (SELECT end_date
+ FROM projects
+ WHERE id = _project_id)::DATE
+ AND CASE
+ WHEN (_archived IS TRUE) THEN t.project_id IS NOT NULL
+ ELSE t.archived IS FALSE END) AS deadline_logged_hours,
+ (SELECT end_date FROM projects WHERE id = _project_id) AS project_end_date,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS tasks
+ FROM (SELECT id,
+ name,
+ status_id,
+ start_date,
+ end_date,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
+
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND end_date::DATE > (SELECT end_date
+ FROM projects
+ WHERE id = _project_id)::DATE) r)) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_gantt_tasks(_project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT p.id,
+ p.name,
+ 0 AS level,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pm.id,
+ pm.team_member_id,
+ pm.project_access_level_id,
+ (SELECT name
+ FROM job_titles
+ WHERE job_titles.id = tm.job_title_id) AS job_title,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ u.avatar_url,
+ (SELECT email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT name
+ FROM project_access_levels
+ WHERE project_access_levels.id = pm.project_access_level_id) AS access_level,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT t.id,
+ t.name,
+ t.start_date AS start,
+ t.project_id,
+ t.priority_id,
+ t.done,
+ t.end_date AS "end",
+ (SELECT color_code
+ FROM projects
+ WHERE projects.id = t.project_id) AS color_code,
+ t.status_id,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT ARRAY_AGG(ROW_TO_JSON(rec))
+ FROM (SELECT project_member_id AS id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ u2.avatar_url,
+ (SELECT team_member_info_view.email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id)
+ FROM tasks_assignees
+ INNER JOIN project_members pm ON pm.id = tasks_assignees.project_member_id
+ INNER JOIN team_members tm2 ON pm.team_member_id = tm2.id
+ LEFT JOIN users u2 ON tm2.user_id = u2.id
+ WHERE project_id = _project_id::UUID
+ AND project_member_id = pm.id
+ AND t.id = tasks_assignees.task_id
+ ORDER BY name) rec) AS assignees
+ FROM tasks_assignees ta,
+ tasks t
+ WHERE t.archived IS FALSE
+ AND ta.project_member_id = pm.id
+ AND t.id = ta.task_id
+ ORDER BY start_date) rec) AS tasks
+ FROM project_members pm
+ INNER JOIN team_members tm ON pm.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = p.id) rec) AS team_members
+ FROM projects p
+ WHERE p.id = _project_id::UUID) rec;
+
+ RETURN _tasks;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_member_insights(_project_id uuid, _archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM (SELECT (SELECT COUNT(*) FROM project_members WHERE project_id = _project_id) AS total_members_count,
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_id = _project_id
+ AND team_member_id NOT IN
+ (SELECT team_member_id
+ FROM tasks_assignees
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE tasks.project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END))) AS unassigned_members,
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_id = _project_id
+ AND team_member_id IN
+ (SELECT team_member_id
+ FROM tasks_assignees
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE tasks.project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND tasks.end_date::DATE < NOW()::DATE))) AS overdue_members) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_members(_project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _output JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (
+ --
+ SELECT (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
+ u.avatar_url
+ FROM project_members
+ INNER JOIN team_members tm ON project_members.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = _project_id
+ --
+ ) rec
+ INTO _output;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_overview_data(_project_id uuid, _archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM (SELECT (SELECT COUNT(*) FROM tasks WHERE project_id = _project_id) AS total_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND archived IS TRUE) AS archived_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE completed_at > CURRENT_DATE - INTERVAL '7 days'
+ AND project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END) AS last_week_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND parent_task_id IS NOT NULL
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END) AS sub_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS TRUE) AS completed_tasks_color_code,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_doing IS TRUE))) AS pending_tasks_count,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_doing IS TRUE) AS pending_tasks_color_code,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND is_completed(status_id, project_id) IS FALSE) AS todo_tasks_count,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_todo IS TRUE) AS todo_tasks_color_code,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND end_date::DATE < NOW()::DATE
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS FALSE))) AS overdue_count,
+ (SELECT SUM(total_minutes)
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END) AS total_minutes_sum,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ CROSS JOIN tasks t
+ WHERE task_id = t.id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND t.project_id = _project_id) AS time_spent_sum) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_project_wise_resources(_start_date date, _end_date date, _team_id uuid) RETURNS text
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _projects JSON;
+
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ color_code,
+ FALSE AS collapsed,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT date_series,
+ project_id,
+ SUM(total_minutes / 60),
+ JSON_AGG(JSON_BUILD_OBJECT('id', tasks_.id,
+ 'name', tasks_.name
+ )) AS scheduled_tasks
+ FROM GENERATE_SERIES(
+ _start_date::DATE,
+ _end_date::DATE,
+ '1 day'
+ ) AS date_series
+ CROSS JOIN (SELECT id, name, project_id, total_minutes, start_date, end_date
+ FROM tasks) AS tasks_
+ WHERE (date_series >= tasks_.start_date::DATE AND date_series <= tasks_.end_date::DATE)
+ AND tasks_.project_id = projects.id
+ GROUP BY date_series, project_id) rec) AS schedule,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT team_member_id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT date_series,
+ project_id,
+ SUM(total_minutes / 60),
+ JSON_AGG(JSON_BUILD_OBJECT('id', tasks_.id,
+ 'name', tasks_.name
+ )) AS scheduled_tasks
+ FROM GENERATE_SERIES(
+ _start_date::DATE,
+ _end_date::DATE,
+ '1 day'
+ ) AS date_series
+ CROSS JOIN (SELECT id, name, project_id, total_minutes, start_date, end_date
+ FROM tasks,
+ tasks_assignees
+ WHERE task_id = tasks.id
+ AND tasks_assignees.team_member_id = project_members.team_member_id) AS tasks_
+ WHERE (date_series >= tasks_.start_date::DATE AND
+ date_series <= tasks_.end_date::DATE)
+ AND tasks_.project_id = projects.id
+ GROUP BY date_series, project_id) rec) AS tasks,
+ (SELECT name
+ FROM users
+ WHERE users.id =
+ (SELECT user_id
+ FROM team_members
+ WHERE team_members.id = project_members.team_member_id)),
+ (SELECT email
+ FROM email_invitations
+ WHERE project_members.team_member_id = email_invitations.team_member_id) AS invitee_email,
+ (SELECT avatar_url
+ FROM users
+ WHERE users.id =
+ (SELECT user_id
+ FROM team_members
+ WHERE team_members.id = project_members.team_member_id))
+ FROM project_members
+ WHERE project_id = projects.id) rec) AS project_members,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT date_series,
+ project_id,
+ SUM(total_minutes / 60),
+ JSON_AGG(JSON_BUILD_OBJECT('id', tasks_.id,
+ 'name', tasks_.name
+ )) AS scheduled_tasks
+ FROM GENERATE_SERIES(
+ _start_date::DATE,
+ _end_date,
+ '1 day'
+ ) AS date_series
+ CROSS JOIN (SELECT id, name, project_id, total_minutes, start_date, end_date
+ FROM tasks
+ WHERE tasks.project_id = project_id
+ AND tasks.id NOT IN (SELECT task_id FROM tasks_assignees)) AS tasks_
+ WHERE (date_series >= tasks_.start_date::DATE AND date_series <= tasks_.end_date::DATE)
+ AND tasks_.project_id = projects.id
+ GROUP BY date_series, project_id) rec) AS unassigned_tasks
+ FROM projects
+ WHERE team_id = _team_id
+ AND id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id)
+ ORDER BY updated_at DESC) rec
+ INTO _projects;
+
+ RETURN _projects;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_member_current_doing_tasks(_team_member_id uuid, _user_id uuid, _include_archived boolean, _limit numeric, _offset numeric) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM ((SELECT t.id,
+ t.name AS task,
+ p.name AS project,
+ p.id AS project_id,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ t.end_date,
+ t.updated_at AS last_updated
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE ta.team_member_id = _team_member_id
+ AND p.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id) END
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ ORDER BY end_date DESC
+ LIMIT _limit OFFSET _offset)) rec) AS data
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE ta.team_member_id = _team_member_id
+ AND p.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id) END
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))) rec;
+ RETURN _result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_member_overdue_tasks(_team_member_id uuid, _user_id uuid, _include_archived boolean, _limit numeric, _offset numeric) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM ((SELECT t.id,
+ t.name AS task,
+ p.name AS project,
+ p.id AS project_id,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ t.end_date,
+ t.updated_at AS last_updated,
+ t.end_date AS due_date
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE ta.team_member_id = _team_member_id
+ AND p.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id) END
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.end_date::DATE < NOW()::DATE
+ ORDER BY end_date DESC
+ LIMIT _limit OFFSET _offset)) rec) AS data
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE ta.team_member_id = _team_member_id
+ AND p.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id) END
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND t.end_date::DATE < NOW()::DATE) rec;
+ RETURN _result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_member_recently_logged_tasks(_team_member_id uuid, _user_id uuid, _include_archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM ((SELECT DISTINCT twl.task_id AS id,
+ t.name,
+ (SELECT SUM(twl.time_spent)) AS logged_time,
+ p.name AS project_name,
+ p.id AS project_id,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = (SELECT status_id FROM tasks WHERE id = twl.task_id)) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ t.end_date::DATE,
+ (SELECT MAX(created_at)
+ FROM task_work_log
+ WHERE task_work_log.task_id = twl.task_id) AS logged_timestamp
+ FROM task_work_log twl
+ LEFT JOIN tasks t ON twl.task_id = t.id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE user_id = (SELECT user_id FROM team_members WHERE id = _team_member_id)
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+
+ -- check if the include_archived flag is true or false
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id) END
+
+ GROUP BY task_id, t.name, project_id, t.end_date, p.name, p.team_id, t.status_id, p.id
+ ORDER BY (SELECT MAX(created_at) FROM task_work_log WHERE task_work_log.task_id = twl.task_id) DESC
+ LIMIT 10)) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_members_stats(_team_member_id uuid, _include_archived boolean, _user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+ _teams UUID[];
+
+BEGIN
+ -- SELECT the team_id to select into the _teams variable so the function doesn't have to run it multiple times
+ SELECT ARRAY_AGG(team_id)
+ INTO _teams
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE));
+ /*
+ ##### id IN (SELECT UNNEST(_teams)) #####
+
+ the above piece of query is added to all the queries below to ensure that the statistics that are shown are related
+ to the teams that the user is either an admin or owner of the team
+ */
+
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM ((SELECT (SELECT name FROM team_member_info_view WHERE team_member_id = _team_member_id),
+
+ -- get total teams that the user is added to
+ (SELECT COUNT(*)
+ FROM teams
+ WHERE id IN (SELECT team_id
+ FROM team_members
+ WHERE team_members.user_id =
+ (SELECT user_id
+ FROM team_members
+ WHERE id = _team_member_id))
+ AND id IN (SELECT UNNEST(_teams))) AS total_teams,
+
+ -- select total projects the user is added to
+ (SELECT COUNT(*)
+ FROM project_members
+ LEFT JOIN projects p ON project_members.project_id = p.id
+ WHERE team_member_id IN (SELECT id
+ FROM team_members
+ WHERE user_id =
+ (SELECT user_id FROM team_members WHERE id = _team_member_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+ AND p.team_id IN (SELECT team_id
+ FROM projects
+ WHERE id IN (SELECT project_id
+ FROM tasks
+ WHERE id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = (SELECT user_id
+ FROM team_members
+ WHERE id = _team_member_id)))))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN project_members.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = project_members.project_id) END) AS project_members,
+
+ -- select the total number of seconds estimated for the tasks that the user is assigned to
+ (SELECT SUM(total_minutes * 60)
+ FROM tasks
+ LEFT JOIN projects p ON tasks.project_id = p.id
+ WHERE EXISTS(SELECT 1
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN tasks.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = tasks.project_id) END) AS total_estimated,
+
+ -- select the total logged time for the tasks that the user is assigned to
+ (SELECT SUM(time_spent)
+ FROM tasks
+ LEFT JOIN task_work_log twl ON tasks.id = twl.task_id
+ LEFT JOIN projects p ON tasks.project_id = p.id
+ WHERE EXISTS(SELECT 1
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN tasks.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = tasks.project_id) END) AS total_logged,
+
+ -- select the total tasks that the user is assigned to
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN projects p ON tasks.project_id = p.id
+ WHERE EXISTS(SELECT 1
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN tasks.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = tasks.project_id) END) AS total_tasks,
+
+ -- select the total tasks that the user has completed
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN projects p ON tasks.project_id = p.id
+
+ WHERE EXISTS(SELECT 1
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+
+ AND tasks.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN tasks.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = tasks.project_id) END) AS total_tasks_completed,
+
+ -- select the total tasks that are overdue
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN projects p ON tasks.project_id = p.id
+
+ WHERE EXISTS(SELECT 1
+ FROM tasks_assignees
+ WHERE tasks.id = tasks_assignees.task_id
+ AND team_member_id IN
+ (SELECT id
+ FROM team_members
+ WHERE user_id = _user_id))
+
+ -- check if the team is a team that the user is either an admin or owner
+ AND p.team_id IN (SELECT UNNEST(_teams))
+
+ AND tasks.end_date::DATE < NOW()::DATE
+ AND tasks.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN tasks.project_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = tasks.project_id) END) AS overdue_tasks)) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_overview_stats(_user_id uuid, _include_archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+
+BEGIN
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM ((SELECT (SELECT COUNT(*)
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE))) AS total_teams,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END) AS total_projects,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END
+ AND status_id IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name NOT IN ('Completed', 'Cancelled'))) AS active_projects,
+ (SELECT COUNT(*)
+ FROM (SELECT DISTINCT user_id
+ FROM team_members tm
+ LEFT JOIN email_invitations ei ON tm.id = ei.team_member_id
+ WHERE tm.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))) AS members) AS total_members,
+ (SELECT COUNT(*)
+ FROM (SELECT DISTINCT user_id
+ FROM team_members tm
+ LEFT JOIN email_invitations ei ON tm.id = ei.team_member_id
+ WHERE tm.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND team_member_id NOT IN
+ (SELECT team_member_id
+ FROM project_members pm
+ WHERE pm.project_id IN
+ (SELECT id FROM projects WHERE projects.team_id = tm.team_id))) AS members) AS unassigned_members,
+ (SELECT COUNT(*)
+ FROM (SELECT DISTINCT user_id
+ FROM team_members tm
+ LEFT JOIN email_invitations ei ON tm.id = ei.team_member_id
+ WHERE tm.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND tm.id IN (SELECT ta.team_member_id
+ FROM tasks_assignees ta
+ LEFT JOIN tasks t
+ ON t.id = ta.task_id AND t.end_date::DATE < NOW()::DATE AND
+ t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))) AS members) AS overdue_task_members,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id IN
+ (SELECT team_id
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND end_date::DATE < NOW()::DATE
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END) AS overdue_projects)) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_reporting_projects_stats(_user_id uuid, _include_archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+ _teams UUID[];
+
+BEGIN
+ -- SELECT the team_id to select into the _teams variable so the function doesn't have to run it multiple times
+ SELECT ARRAY_AGG(team_id)
+ INTO _teams
+ FROM team_members
+ WHERE user_id = _user_id
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE));
+
+ SELECT COALESCE(ROW_TO_JSON(rec), '[]'::JSON)
+ INTO _result
+ FROM ((SELECT (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END
+ AND status_id IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name NOT IN ('Completed', 'Cancelled'))) AS active_projects,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND end_date::DATE < NOW()::DATE
+ AND CASE
+ WHEN (_include_archived IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END) AS overdue_projects,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (_include_archived IS TRUE)
+ THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = _user_id) END)))::INT AS total_logged,
+ (SELECT SUM(total_minutes * 60)
+ FROM tasks
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (TRUE IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = _user_id) END))::INT AS total_estimated,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id IN (SELECT id
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (TRUE IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = _user_id) END))::INT AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id IN (SELECT id
+ FROM projects
+ WHERE team_id IN (SELECT UNNEST(_teams))
+ AND CASE
+ WHEN (TRUE IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = _user_id) END)
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = tasks.project_id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)))::INT AS completed_tasks_count)) rec;
+ RETURN _result;
+
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_resource_gantt_tasks(_user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+ _team_id UUID;
+BEGIN
+
+ SELECT active_team FROM users WHERE id = _user_id::UUID INTO _team_id;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT projects.id,
+ projects.name,
+ 0 AS level,
+ FALSE AS collapsed,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pm.id,
+ pm.team_member_id,
+ pm.project_access_level_id,
+ FALSE AS overdue,
+ (SELECT name
+ FROM job_titles
+ WHERE job_titles.id = tm.job_title_id) AS job_title,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ u.avatar_url,
+ (SELECT name
+ FROM project_access_levels
+ WHERE project_access_levels.id = pm.project_access_level_id) AS access_level,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT t.id,
+ t.id AS task_id,
+ t.name,
+ t.start_date AS start,
+ t.project_id,
+ t.priority_id,
+ t.done,
+ t.end_date AS "end",
+ 0 AS progress,
+ 2 AS level,
+ TRUE AS "showOnGraph",
+ t.status_id,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM projects
+ WHERE projects.id = t.project_id) AS color_code,
+ (SELECT ARRAY_AGG(ROW_TO_JSON(rec))
+ FROM (SELECT project_member_id AS id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id)
+ FROM tasks_assignees
+ INNER JOIN project_members pm ON pm.id = tasks_assignees.project_member_id
+ INNER JOIN team_members tm2 ON pm.team_member_id = tm2.id
+ LEFT JOIN users u2 ON tm2.user_id = u2.id
+ WHERE project_id = t.project_id
+ AND project_member_id = pm.id
+ AND t.id = tasks_assignees.task_id
+ ORDER BY name) rec) AS assignees
+ FROM tasks_assignees ta,
+ tasks t
+ WHERE t.archived IS FALSE
+ AND ta.project_member_id = pm.id
+ AND t.id = ta.task_id
+ ORDER BY start_date) rec) AS tasks
+ FROM project_members pm
+ INNER JOIN team_members tm ON pm.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = projects.id) rec) AS team_members
+ FROM projects
+ WHERE team_id = _team_id
+ AND (CASE
+ WHEN (is_owner(_user_id, _team_id) OR is_admin(_user_id, _team_id)) THEN TRUE
+ ELSE is_member_of_project(projects.id, _user_id, _team_id) END)
+ ORDER BY NAME) rec;
+
+ RETURN _tasks;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_selected_tasks(_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT t.id,
+ t.name,
+ t.start_date AS start,
+ t.end_date AS "end",
+ t.project_id,
+ (SELECT name FROM task_priorities WHERE task_priorities.id = t.priority_id) AS priority,
+ t.priority_id,
+ t.done,
+ t.created_at,
+ t.status_id,
+ (SELECT ARRAY_AGG(ROW_TO_JSON(rec))
+ FROM (SELECT project_member_id AS id,
+ u2.name AS name,
+ (SELECT avatar_url FROM users WHERE id = tm2.user_id),
+ COALESCE((u2.email), (SELECT email
+ FROM email_invitations
+ WHERE email_invitations.team_member_id = tm2.id)) AS email
+ FROM tasks_assignees
+ INNER JOIN project_members pm ON pm.id = tasks_assignees.project_member_id
+ INNER JOIN team_members tm2 ON pm.team_member_id = tm2.id
+ LEFT JOIN users u2 ON tm2.user_id = u2.id
+ WHERE project_id = _id::UUID
+ AND project_member_id = pm.id
+ AND t.id = tasks_assignees.task_id
+ ORDER BY name) rec) AS assignees
+ FROM tasks t
+ WHERE archived IS FALSE
+ AND project_id = _id::UUID
+ AND (t.start_date IS NOT NULL
+ OR t.end_date IS NOT NULL
+ OR t.id IN (SELECT task_id FROM tasks_assignees))) rec;
+
+ RETURN _tasks;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_single_pt_task(_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT id,
+ name,
+ cpt_tasks.template_id AS template_id,
+ cpt_tasks.parent_task_id,
+ cpt_tasks.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM cpt_tasks WHERE id = cpt_tasks.parent_task_id) AS parent_task_name,
+ (SELECT COUNT('*')
+ FROM cpt_tasks
+ WHERE parent_task_id = cpt_tasks.id) AS sub_tasks_count,
+ cpt_tasks.status_id AS status,
+ (SELECT name FROM cpt_task_statuses WHERE id = cpt_tasks.status_id) AS status_name,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = cpt_tasks.status_id)) AS status_color,
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = cpt_tasks.status_id)) r) AS status_category,
+ (SELECT name
+ FROM cpt_phases
+ WHERE id = (SELECT phase_id FROM cpt_task_phases WHERE task_id = cpt_tasks.id)) AS phase_name,
+ (SELECT phase_id FROM cpt_task_phases WHERE task_id = cpt_tasks.id) AS phase_id,
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT cpt_task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = cpt_task_labels.label_id) AS name,
+ (SELECT color_code FROM team_labels WHERE id = cpt_task_labels.label_id)
+ FROM cpt_task_labels
+ WHERE task_id = cpt_tasks.id
+ ORDER BY name) r) AS labels,
+ (SELECT id FROM task_priorities WHERE id = cpt_tasks.priority_id) AS priority,
+ (SELECT name FROM task_priorities WHERE id = cpt_tasks.priority_id) AS priority_name,
+ (SELECT value FROM task_priorities WHERE id = cpt_tasks.priority_id) AS priority_value,
+ total_minutes,
+ sort_order,
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT cpt_task_statuses.id AS id,
+ cpt_task_statuses.name AS name,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = cpt_task_statuses.category_id)
+ FROM cpt_task_statuses
+ WHERE cpt_task_statuses.template_id = cpt_tasks.template_id) r) AS template_statuses
+ FROM cpt_tasks
+ WHERE id = _id) rec;
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_single_task(_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+
+ SELECT ROW_TO_JSON(rec)
+ INTO _result
+ FROM (SELECT id,
+ name,
+ (SELECT name FROM projects WHERE project_id = projects.id) AS project_name,
+ CONCAT((SELECT key FROM projects WHERE id = project_id), '-', task_no) AS task_key,
+ tasks.project_id AS project_id,
+ tasks.parent_task_id,
+ tasks.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM tasks WHERE id = tasks.parent_task_id) AS parent_task_name,
+ (SELECT COUNT('*')
+ FROM tasks
+ WHERE parent_task_id = tasks.id
+ AND archived IS FALSE) AS sub_tasks_count,
+
+ tasks.status_id AS status,
+ tasks.archived,
+
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ (SELECT name FROM task_priorities WHERE id = tasks.priority_id) AS priority_name,
+
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
+
+ (SELECT get_task_assignees(tasks.id)) AS assignees,
+
+ (SELECT name
+ FROM project_phases
+ WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = tasks.id)) AS phase_name,
+ (SELECT color_code
+ FROM project_phases
+ WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = tasks.id)) AS phase_color_code,
+ (SELECT phase_id FROM task_phase WHERE task_id = tasks.id) AS phase_id,
+
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) r) AS status_category,
+
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = task_labels.label_id) AS name,
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = tasks.id
+ ORDER BY name) r) AS labels,
+
+ (SELECT name FROM users WHERE id = reporter_id) AS reporter,
+ (SELECT id FROM task_priorities WHERE id = tasks.priority_id) AS priority,
+ (SELECT value FROM task_priorities WHERE id = tasks.priority_id) AS priority_value,
+ total_minutes,
+ (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) AS total_minutes_spent,
+ start_date,
+ end_date,
+ sort_order,
+ (SELECT color_code FROM projects WHERE projects.id = tasks.project_id) AS project_color,
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT task_statuses.id AS id,
+ task_statuses.name AS name,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = task_statuses.category_id)
+ FROM task_statuses
+ WHERE task_statuses.project_id = tasks.project_id) r) AS project_statuses
+ FROM tasks
+ WHERE id = _id) rec;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_task_assignees(_task_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _output JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT team_member_id,
+ project_member_id,
+ (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE team_id = tm.team_id
+ AND notification_settings.user_id = u.id) AS email_notifications_enabled,
+ u.avatar_url,
+ u.id AS user_id,
+ u.email,
+ u.socket_id AS socket_id,
+ tm.team_id AS team_id
+ FROM tasks_assignees
+ INNER JOIN team_members tm ON tm.id = tasks_assignees.team_member_id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE task_id = _task_id) rec
+ INTO _output;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_task_complete_info(_task_id uuid, _status_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _color_code TEXT;
+ _members JSON;
+BEGIN
+ SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = _status_id)
+ INTO _color_code;
+
+ SELECT get_task_assignees(_task_id) INTO _members;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'color_code', _color_code,
+ 'members', _members
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_task_complete_ratio(_task_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _parent_task_done FLOAT = 0;
+ _sub_tasks_done FLOAT = 0;
+ _sub_tasks_count FLOAT = 0;
+ _total_completed FLOAT = 0;
+ _total_tasks FLOAT = 0;
+ _ratio FLOAT = 0;
+BEGIN
+ SELECT (CASE
+ WHEN EXISTS(SELECT 1
+ FROM tasks_with_status_view
+ WHERE tasks_with_status_view.task_id = _task_id
+ AND is_done IS TRUE) THEN 1
+ ELSE 0 END)
+ INTO _parent_task_done;
+ SELECT COUNT(*) FROM tasks WHERE parent_task_id = _task_id AND archived IS FALSE INTO _sub_tasks_count;
+
+ SELECT COUNT(*)
+ FROM tasks_with_status_view
+ WHERE parent_task_id = _task_id
+ AND is_done IS TRUE
+ INTO _sub_tasks_done;
+
+ _total_completed = _parent_task_done + _sub_tasks_done;
+ _total_tasks = _sub_tasks_count + 1; -- +1 for the parent task
+ _ratio = (_total_completed / _total_tasks) * 100;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'ratio', _ratio,
+ 'total_completed', _total_completed,
+ 'total_tasks', _total_tasks
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_task_form_view_model(_user_id uuid, _team_id uuid, _task_id uuid, _project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _priorities JSON;
+ _projects JSON;
+ _statuses JSON;
+ _team_members JSON;
+ _assignees JSON;
+ _phases JSON;
+BEGIN
+
+ -- Select task info
+ SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ INTO _task
+ FROM (SELECT id,
+ name,
+ description,
+ start_date,
+ end_date,
+ done,
+ total_minutes,
+ priority_id,
+ project_id,
+ created_at,
+ updated_at,
+ status_id,
+ parent_task_id,
+ sort_order,
+ (SELECT phase_id FROM task_phase WHERE task_id = tasks.id) AS phase_id,
+ CONCAT((SELECT key FROM projects WHERE id = tasks.project_id), '-', task_no) AS task_key,
+ (SELECT start_time
+ FROM task_timers
+ WHERE task_id = tasks.id
+ AND user_id = _user_id) AS timer_start_time,
+ parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT COUNT('*')
+ FROM tasks
+ WHERE parent_task_id = tasks.id
+ AND archived IS FALSE) AS sub_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE (tt.parent_task_id = tasks.id OR tt.task_id = tasks.id)
+ AND tt.is_done IS TRUE)
+ AS completed_count,
+ (SELECT COUNT(*) FROM task_attachments WHERE task_id = tasks.id) AS attachments_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON)
+ FROM (SELECT task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = task_labels.label_id),
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = tasks.id
+ ORDER BY name) r) AS labels,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
+ (SELECT COUNT(*) FROM tasks WHERE parent_task_id = _task_id) AS sub_tasks_count,
+ (SELECT name FROM users WHERE id = tasks.reporter_id) AS reporter,
+ (SELECT get_task_assignees(tasks.id)) AS assignees,
+ (SELECT id FROM team_members WHERE user_id = _user_id AND team_id = _team_id) AS team_member_id
+ FROM tasks
+ WHERE id = _task_id) rec;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _priorities
+ FROM (SELECT id, name FROM task_priorities ORDER BY value) rec;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _phases
+ FROM (SELECT id, name FROM project_phases WHERE project_id = _project_id ORDER BY name) rec;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _projects
+ FROM (SELECT id, name
+ FROM projects
+ WHERE team_id = _team_id
+ AND (CASE
+ WHEN (is_owner(_user_id, _team_id) OR is_admin(_user_id, _team_id) IS TRUE) THEN TRUE
+ ELSE is_member_of_project(projects.id, _user_id, _team_id) END)
+ ORDER BY name) rec;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _statuses
+ FROM (SELECT id, name FROM task_statuses WHERE project_id = _project_id) rec;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _team_members
+ FROM (SELECT team_members.id,
+ (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = team_members.id),
+ (SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = team_members.id),
+ (SELECT avatar_url
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = team_members.id),
+ CASE WHEN EXISTS (
+ SELECT 1 FROM email_invitations WHERE team_member_id = team_members.id
+ ) THEN TRUE
+ ELSE FALSE
+ END AS is_pending
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ WHERE team_id = _team_id
+ AND team_members.active IS TRUE) rec;
+
+ SELECT get_task_assignees(_task_id) INTO _assignees;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'task', _task,
+ 'priorities', _priorities,
+ 'projects', _projects,
+ 'statuses', _statuses,
+ 'team_members', _team_members,
+ 'assignees', _assignees,
+ 'phases', _phases
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_task_updates() RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (SELECT name,
+ email,
+ (SELECT id
+ FROM team_members
+ WHERE team_id = users.active_team
+ AND user_id = users.id) AS team_member_id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS teams
+ FROM (SELECT id,
+ name,
+ (SELECT team_member_id
+ FROM team_member_info_view
+ WHERE team_id = teams.id
+ AND user_id = users.id) AS team_member_id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS projects
+ FROM (SELECT id,
+ name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON) AS tasks
+ FROM (SELECT t.id,
+ t.name AS name,
+ (SELECT name FROM users WHERE id = task_updates.reporter_id) AS updater_name,
+ (SELECT STRING_AGG(DISTINCT
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_id = tasks_assignees.team_member_id),
+ ', ')
+ FROM tasks_assignees
+ WHERE task_id = task_updates.task_id) AS members
+ FROM task_updates
+ INNER JOIN tasks t ON task_updates.task_id = t.id
+ WHERE task_updates.user_id = users.id
+ AND task_updates.project_id = projects.id
+ AND task_updates.type = 'ASSIGN'
+ AND is_sent IS FALSE
+ ORDER BY task_updates.created_at) r)
+ FROM projects
+ WHERE team_id = teams.id
+ AND EXISTS(SELECT 1
+ FROM task_updates
+ WHERE project_id = projects.id
+ AND type = 'ASSIGN'
+ AND is_sent IS FALSE)) r)
+ FROM teams
+ WHERE EXISTS(SELECT 1 FROM team_members WHERE team_id = teams.id AND user_id = users.id)
+ AND (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE team_id = teams.id
+ AND user_id = users.id) IS TRUE) r)
+ FROM users
+ WHERE EXISTS(SELECT 1 FROM task_updates WHERE user_id = users.id)) rec;
+
+ UPDATE task_updates SET is_sent = TRUE;
+
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_tasks_by_project_member(_project_id uuid, _team_member_id uuid, _archived boolean) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r))), '[]'::JSON)
+ INTO _result
+ FROM (SELECT id,
+ name,
+ status_id,
+ (SELECT name FROM task_statuses WHERE status_id = task_statuses.id) AS status,
+ start_date,
+ end_date,
+ completed_at,
+ total_minutes,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
+ CASE
+ WHEN CURRENT_DATE::DATE > end_date::DATE
+ AND status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS FALSE))
+ THEN FALSE
+ ELSE
+ CASE
+ WHEN CURRENT_DATE::DATE > end_date::DATE
+ THEN TRUE
+ ELSE FALSE
+ END END AS is_overdue,
+ CASE
+ WHEN status_id
+ NOT IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS FALSE))
+ THEN 0
+ ELSE
+ CASE
+ WHEN CURRENT_DATE::DATE > end_date::DATE
+ THEN NOW()::DATE - end_date::DATE
+ ELSE 0
+ END END AS days_overdue,
+ CASE
+ WHEN (NOW()::DATE > end_date)
+ AND status_id
+ NOT IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS FALSE))
+ THEN 0
+ ELSE COALESCE(CASE
+ WHEN
+ ((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) >
+ (total_minutes * 60)) THEN
+ (SELECT SUM(time_spent) * 60
+ FROM task_work_log
+ WHERE task_id = tasks.id) -
+ (total_minutes * 60)
+ ELSE 0 END, 0) END AS overlogged_time,
+ COALESCE(completed_at::DATE - end_date::DATE, 0) AS late_days
+ FROM tasks
+ WHERE project_id = _project_id
+ AND CASE
+ WHEN (_archived IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND id IN
+ (SELECT task_id FROM tasks_assignees WHERE team_member_id = _team_member_id)) r;
+ RETURN _result;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_tasks_by_status(_id uuid, _status text) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+ _team_id UUID;
+BEGIN
+ SELECT team_id FROM projects WHERE id = _id INTO _team_id;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT id,
+ name,
+
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
+ (SELECT COUNT('*') FROM tasks WHERE parent_task_id = t.id) AS sub_tasks_count,
+
+ status_id AS status,
+ sort_order,
+
+ (SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE (tt.parent_task_id = t.id OR tt.task_id = t.id)
+ AND tt.is_done IS TRUE)
+ AS completed_count,
+
+ (SELECT get_task_assignees(t.id)) AS assignees,
+
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = task_labels.label_id),
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = t.id
+ ORDER BY name) r) AS labels,
+
+ (SELECT name FROM users WHERE id = reporter_id) AS reporter,
+ (SELECT task_priorities.name FROM task_priorities WHERE id = t.priority_id) AS priority,
+ start_date,
+ end_date
+ FROM tasks t
+ WHERE archived IS FALSE
+ AND (project_id = _id)
+ AND (t.status_id IN (SELECT id FROM task_statuses WHERE name = _status))
+ ORDER BY sort_order) rec;
+
+ RETURN _tasks;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_tasks_total_and_completed_counts(_task_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _total_tasks INT;
+ _total_completed INT;
+BEGIN
+ SELECT COUNT(*)
+ FROM tasks
+ WHERE (parent_task_id = _task_id OR id = _task_id)
+ AND archived IS FALSE
+ INTO _total_tasks;
+
+ SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE (tt.parent_task_id = _task_id
+ OR tt.task_id = _task_id)
+ AND tt.is_done IS TRUE
+ INTO _total_completed;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'total_tasks', _total_tasks,
+ 'total_completed', _total_completed
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_team_id_from_task(_id uuid) RETURNS uuid
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _team_id UUID;
+BEGIN
+ SELECT team_id INTO _team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = _id);
+ RETURN _team_id;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION get_team_members(_team_id uuid, _project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _result JSON;
+BEGIN
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _result
+ FROM (
+ --
+ WITH mbers AS (SELECT team_members.id,
+ tmiv.name AS name,
+ tmiv.email,
+ tmiv.avatar_url,
+ team_members.user_id,
+ EXISTS(SELECT 1
+ FROM project_members
+ WHERE project_id = _project_id
+ AND project_members.team_member_id = team_members.id) AS exists_in_project,
+ 0 AS usage,
+ CASE
+ WHEN EXISTS (SELECT 1
+ FROM email_invitations
+ WHERE team_member_id = team_members.id) THEN TRUE
+ ELSE FALSE
+ END AS is_pending
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
+ WHERE team_members.team_id = _team_id
+ AND active IS TRUE
+ ORDER BY tmiv.name)
+ SELECT id, name, user_id, email, avatar_url, usage, is_pending
+ FROM mbers
+ ORDER BY exists_in_project DESC
+ --
+ ) rec;
+
+ RETURN _result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_team_wise_resources(_start_date date, _end_date date, _team_id uuid) RETURNS text
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _projects JSON;
+
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id,
+ FALSE AS collapsed,
+ (SELECT name FROM users WHERE user_id = users.id),
+ (SELECT email
+ FROM email_invitations
+ WHERE team_members.id = email_invitations.team_member_id) AS invitee_email,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT date_series,
+ SUM(total_minutes / 60),
+ JSON_AGG(JSON_BUILD_OBJECT('id', tasks_.id,
+ 'name', tasks_.name,
+ 'project_id', tasks_.project_id,
+ 'project_name', (SELECT projects.name
+ FROM projects
+ WHERE projects.id = tasks_.project_id)
+ )) AS scheduled_tasks
+ FROM GENERATE_SERIES(
+ _start_date::DATE,
+ _end_date::DATE,
+ '1 day'
+ ) AS date_series
+ CROSS JOIN (SELECT id, name, project_id, total_minutes, start_date, end_date
+ FROM tasks) AS tasks_,
+ tasks_assignees
+ WHERE (date_series BETWEEN tasks_.start_date::DATE AND tasks_.end_date::DATE)
+ AND tasks_assignees.team_member_id = team_members.id
+ AND tasks_.id = tasks_assignees.task_id
+ GROUP BY date_series) rec) AS schedule,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT projects.id,
+ name,
+ projects.color_code,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT date_series,
+ project_id,
+ SUM(total_minutes / 60),
+ JSON_AGG(JSON_BUILD_OBJECT('id', tasks_.id,
+ 'name', tasks_.name
+ )) AS scheduled_tasks
+ FROM GENERATE_SERIES(
+ _start_date::DATE,
+ _end_date::DATE,
+ '1 day'
+ ) AS date_series
+ CROSS JOIN (SELECT id, name, project_id, total_minutes, start_date, end_date
+ FROM tasks) AS tasks_,
+ tasks_assignees
+ WHERE (date_series BETWEEN tasks_.start_date::DATE AND tasks_.end_date::DATE)
+ AND tasks_assignees.team_member_id = team_members.id
+ AND tasks_.id = tasks_assignees.task_id
+ AND tasks_.project_id = projects.id
+ GROUP BY date_series, project_id) rec) AS tasks
+ FROM projects,
+ project_members
+ WHERE project_id = projects.id
+ AND team_members.id IN (project_members.team_member_id)
+ ORDER BY updated_at DESC) rec) AS project_members
+ FROM team_members
+ WHERE team_id = _team_id
+ AND user_id IS NOT NULL
+ ORDER BY (SELECT name FROM users WHERE user_id = users.id)) rec
+ INTO _projects;
+
+ RETURN _projects;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION get_unselected_tasks(_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _tasks JSON;
+BEGIN
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _tasks
+ FROM (SELECT t.id,
+ t.name,
+ t.start_date AT TIME ZONE 'Asia/Colombo' AS start,
+ t.end_date AS "end",
+ t.project_id,
+ (SELECT name FROM task_priorities WHERE task_priorities.id = t.priority_id) AS priority,
+ t.priority_id,
+ t.done,
+ t.created_at,
+ t.status_id,
+ (SELECT ARRAY_AGG(ROW_TO_JSON(rec))
+ FROM (SELECT project_member_id AS id,
+ u2.name AS name,
+ (SELECT avatar_url FROM users WHERE id = tm2.user_id),
+ COALESCE((u2.email), (SELECT email
+ FROM email_invitations
+ WHERE email_invitations.team_member_id = tm2.id)) AS email
+ FROM tasks_assignees
+ INNER JOIN project_members pm ON pm.id = tasks_assignees.project_member_id
+ INNER JOIN team_members tm2 ON pm.team_member_id = tm2.id
+ LEFT JOIN users u2 ON tm2.user_id = u2.id
+ WHERE project_id = _id::UUID
+ AND project_member_id = pm.id
+ AND t.id = tasks_assignees.task_id
+ ORDER BY name) rec) AS assignees
+ FROM tasks t
+ WHERE archived IS FALSE
+ AND project_id = _id::UUID
+ AND (t.start_date IS NULL
+ OR t.end_date IS NULL
+ OR t.id NOT IN (SELECT task_id FROM tasks_assignees))) rec;
+
+ RETURN _tasks;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION handle_on_pt_task_phase_change(_task_id uuid, _phase_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _name TEXT;
+ _color_code TEXT;
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM cpt_task_phases WHERE task_id = _task_id)
+ THEN
+ IF (is_null_or_empty(_phase_id) IS FALSE)
+ THEN
+ INSERT INTO cpt_task_phases (task_id, phase_id) VALUES (_task_id, _phase_id);
+ END IF;
+ ELSE
+ IF (is_null_or_empty(_phase_id) IS TRUE)
+ THEN
+ DELETE FROM cpt_task_phases WHERE task_id = _task_id;
+ ELSE
+ UPDATE cpt_task_phases SET phase_id = _phase_id WHERE task_id = _task_id;
+ END IF;
+ END IF;
+ IF (is_null_or_empty(_phase_id) IS FALSE)
+ THEN
+ SELECT name FROM cpt_phases WHERE id = _phase_id INTO _name;
+ SELECT color_code FROM cpt_phases WHERE id = _phase_id INTO _color_code;
+ END IF;
+ RETURN JSON_BUILD_OBJECT('name', _name, 'color_code', _color_code);
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_on_pt_task_status_change(_task_id uuid, _status_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task_info JSON;
+ _status_category JSON;
+ _task_name TEXT;
+ _previous_status_name TEXT;
+ _new_status_name TEXT;
+BEGIN
+ SELECT name FROM cpt_tasks WHERE id = _task_id INTO _task_name;
+
+ SELECT name
+ FROM cpt_task_statuses
+ WHERE id = (SELECT status_id FROM cpt_tasks WHERE id = _task_id)
+ INTO _previous_status_name;
+
+ SELECT name FROM cpt_task_statuses WHERE id = _status_id INTO _new_status_name;
+
+ IF (_previous_status_name != _new_status_name)
+ THEN
+ UPDATE cpt_tasks SET status_id = _status_id WHERE id = _task_id;
+ END IF;
+
+ SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = _status_id)) r
+ INTO _status_category;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'template_id', (SELECT template_id FROM cpt_tasks WHERE id = _task_id),
+ 'color_code', (SELECT color_code FROM sys_task_status_categories WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = _status_id))::TEXT,
+ 'status_category', _status_category
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_on_task_phase_change(_task_id uuid, _phase_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _name TEXT;
+ _color_code TEXT;
+BEGIN
+ IF NOT EXISTS(SELECT 1 FROM task_phase WHERE task_id = _task_id)
+ THEN
+ IF (is_null_or_empty(_phase_id) IS FALSE)
+ THEN
+ INSERT INTO task_phase (task_id, phase_id) VALUES (_task_id, _phase_id);
+ END IF;
+ ELSE
+ IF (is_null_or_empty(_phase_id) IS TRUE)
+ THEN
+ DELETE FROM task_phase WHERE task_id = _task_id;
+ ELSE
+ UPDATE task_phase SET phase_id = _phase_id WHERE task_id = _task_id;
+ END IF;
+ END IF;
+
+ IF (is_null_or_empty(_phase_id) IS FALSE)
+ THEN
+ SELECT name FROM project_phases WHERE id = _phase_id INTO _name;
+ SELECT color_code FROM project_phases WHERE id = _phase_id INTO _color_code;
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT('name', _name, 'color_code', _color_code);
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_on_task_status_change(_user_id uuid, _task_id uuid, _status_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _updater_name TEXT;
+ _task_name TEXT;
+ _previous_status_name TEXT;
+ _new_status_name TEXT;
+ _message TEXT;
+ _task_info JSON;
+ _status_category JSON;
+ _task_completed_at TIMESTAMPTZ;
+BEGIN
+ SELECT name FROM tasks WHERE id = _task_id INTO _task_name;
+
+ SELECT name
+ FROM task_statuses
+ WHERE id = (SELECT status_id FROM tasks WHERE id = _task_id)
+ INTO _previous_status_name;
+
+ SELECT name FROM task_statuses WHERE id = _status_id INTO _new_status_name;
+
+ IF (_previous_status_name != _new_status_name)
+ THEN
+ UPDATE tasks SET status_id = _status_id WHERE id = _task_id;
+
+ SELECT get_task_complete_info(_task_id, _status_id) INTO _task_info;
+
+ SELECT name FROM users WHERE id = _user_id INTO _updater_name;
+
+ _message = CONCAT(_updater_name, ' transitioned "', _task_name, '" from ', _previous_status_name, ' ⟶ ',
+ _new_status_name);
+ END IF;
+
+ SELECT completed_at FROM tasks WHERE id = _task_id INTO _task_completed_at;
+
+ SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = _status_id)) r
+ INTO _status_category;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'message', _message,
+ 'project_id', (SELECT project_id FROM tasks WHERE id = _task_id),
+ 'parent_done', (CASE
+ WHEN EXISTS(SELECT 1
+ FROM tasks_with_status_view
+ WHERE tasks_with_status_view.task_id = _task_id
+ AND is_done IS TRUE) THEN 1
+ ELSE 0 END),
+ 'color_code', (_task_info ->> 'color_code')::TEXT,
+ 'total_tasks', (_task_info ->> 'total_tasks')::INT,
+ 'total_completed', (_task_info ->> 'total_completed')::INT,
+ 'members', (_task_info ->> 'members')::JSON,
+ 'completed_at', _task_completed_at,
+ 'status_category', _status_category
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_pt_task_list_sort_between_groups(_from_index integer, _to_index integer, _task_id uuid, _template_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+
+ IF (_to_index = -1)
+ THEN
+ _to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM cpt_tasks WHERE template_id = _template_id), 0);
+ END IF;
+
+ IF _to_index > _from_index
+ THEN
+ UPDATE cpt_tasks
+ SET sort_order = sort_order - 1
+ WHERE template_id = _template_id
+ AND sort_order > _from_index
+ AND sort_order < _to_index;
+
+ UPDATE cpt_tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND template_id = _template_id;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE cpt_tasks
+ SET sort_order = sort_order + 1
+ WHERE template_id = _template_id
+ AND sort_order > _to_index
+ AND sort_order < _from_index;
+
+ UPDATE cpt_tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND template_id = _template_id;
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_pt_task_list_sort_inside_group(_from_index integer, _to_index integer, _task_id uuid, _template_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ IF _to_index > _from_index
+ THEN
+ UPDATE cpt_tasks
+ SET sort_order = sort_order - 1
+ WHERE template_id = _template_id
+ AND sort_order > _from_index
+ AND sort_order <= _to_index;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE cpt_tasks
+ SET sort_order = sort_order + 1
+ WHERE template_id = _template_id
+ AND sort_order >= _to_index
+ AND sort_order < _from_index;
+ END IF;
+
+ UPDATE cpt_tasks SET sort_order = _to_index WHERE id = _task_id AND template_id = _template_id;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_pt_task_list_sort_order_change(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _from_index INT;
+ _to_index INT;
+ _task_id UUID;
+ _template_id UUID;
+ _from_group UUID;
+ _to_group UUID;
+ _group_by TEXT;
+BEGIN
+ _template_id = (_body ->> 'template_id')::UUID;
+ _task_id = (_body ->> 'task_id')::UUID;
+ _from_index = (_body ->> 'from_index')::INT;
+ _to_index = (_body ->> 'to_index')::INT;
+ _from_group = (_body ->> 'from_group')::UUID;
+ _to_group = (_body ->> 'to_group')::UUID;
+ _group_by = (_body ->> 'group_by')::TEXT;
+
+ IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
+ THEN
+ IF (_group_by = 'status')
+ THEN
+ UPDATE cpt_tasks SET status_id = _to_group WHERE id = _task_id AND status_id = _from_group;
+ END IF;
+
+ IF (_group_by = 'priority')
+ THEN
+ UPDATE cpt_tasks SET priority_id = _to_group WHERE id = _task_id AND priority_id = _from_group;
+ END IF;
+
+ IF (_group_by = 'phase')
+ THEN
+ IF (is_null_or_empty(_to_group) IS FALSE)
+ THEN
+ INSERT INTO cpt_task_phases (task_id, phase_id)
+ VALUES (_task_id, _to_group)
+ ON CONFLICT (task_id) DO UPDATE SET phase_id = _to_group;
+ END IF;
+ IF (is_null_or_empty(_to_group) IS TRUE)
+ THEN
+ DELETE
+ FROM cpt_task_phases
+ WHERE task_id = _task_id;
+ END IF;
+
+ END IF;
+
+ IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
+ THEN
+ PERFORM handle_pt_task_list_sort_inside_group(_from_index, _to_index, _task_id, _template_id);
+ ELSE
+ PERFORM handle_pt_task_list_sort_between_groups(_from_index, _to_index, _task_id, _template_id);
+ END IF;
+ ELSE
+ PERFORM handle_pt_task_list_sort_inside_group(_from_index, _to_index, _task_id, _template_id);
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+
+ IF (_to_index = -1)
+ THEN
+ _to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
+ END IF;
+
+ IF _to_index > _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order - 1
+ WHERE project_id = _project_id
+ AND sort_order > _from_index
+ AND sort_order < _to_index;
+
+ UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order + 1
+ WHERE project_id = _project_id
+ AND sort_order > _to_index
+ AND sort_order < _from_index;
+
+ UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ IF _to_index > _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order - 1
+ WHERE project_id = _project_id
+ AND sort_order > _from_index
+ AND sort_order <= _to_index;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order + 1
+ WHERE project_id = _project_id
+ AND sort_order >= _to_index
+ AND sort_order < _from_index;
+ END IF;
+
+ UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_task_list_sort_order_change(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _from_index INT;
+ _to_index INT;
+ _task_id UUID;
+ _project_id UUID;
+ _from_group UUID;
+ _to_group UUID;
+ _group_by TEXT;
+BEGIN
+ _project_id = (_body ->> 'project_id')::UUID;
+ _task_id = (_body ->> 'task_id')::UUID;
+
+ _from_index = (_body ->> 'from_index')::INT; -- from sort_order
+ _to_index = (_body ->> 'to_index')::INT; -- to sort_order
+
+ _from_group = (_body ->> 'from_group')::UUID;
+ _to_group = (_body ->> 'to_group')::UUID;
+
+ _group_by = (_body ->> 'group_by')::TEXT;
+
+ IF (_from_group <> _to_group OR (_from_group <> _to_group) IS NULL)
+ THEN
+ IF (_group_by = 'status')
+ THEN
+ UPDATE tasks SET status_id = _to_group WHERE id = _task_id AND status_id = _from_group;
+ END IF;
+
+ IF (_group_by = 'priority')
+ THEN
+ UPDATE tasks SET priority_id = _to_group WHERE id = _task_id AND priority_id = _from_group;
+ END IF;
+
+ IF (_group_by = 'phase')
+ THEN
+ IF (is_null_or_empty(_to_group) IS FALSE)
+ THEN
+ INSERT INTO task_phase (task_id, phase_id)
+ VALUES (_task_id, _to_group)
+ ON CONFLICT (task_id) DO UPDATE SET phase_id = _to_group;
+ END IF;
+ IF (is_null_or_empty(_to_group) IS TRUE)
+ THEN
+ DELETE
+ FROM task_phase
+ WHERE task_id = _task_id;
+ END IF;
+ END IF;
+
+ IF ((_body ->> 'to_last_index')::BOOLEAN IS TRUE AND _from_index < _to_index)
+ THEN
+ PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
+ ELSE
+ PERFORM handle_task_list_sort_between_groups(_from_index, _to_index, _task_id, _project_id);
+ END IF;
+ ELSE
+ PERFORM handle_task_list_sort_inside_group(_from_index, _to_index, _task_id, _project_id);
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_task_name_change(_task_id uuid, _task_name text, _user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _name TEXT;
+ _previous_name TEXT;
+ _user_name TEXT;
+ _message TEXT;
+ _assignees JSON;
+BEGIN
+
+ IF (is_null_or_empty(_task_name))
+ THEN
+ SELECT name FROM tasks WHERE id = _task_id INTO _name;
+ ELSE
+ SELECT name FROM tasks WHERE id = _task_id INTO _previous_name;
+ UPDATE tasks SET name = _task_name WHERE id = _task_id RETURNING name INTO _name;
+ END IF;
+
+ IF (_name != _previous_name)
+ THEN
+ SELECT get_task_assignees(_task_id) INTO _assignees;
+
+ SELECT name FROM users WHERE id = _user_id INTO _user_name;
+
+ _message = CONCAT(_user_name, ' has renamed "', _previous_name, '" → "', _name, '"');
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'name', _name,
+ 'previous_name', _previous_name,
+ 'project_id', (SELECT project_id FROM tasks WHERE id = _task_id),
+ 'message', _message,
+ 'members', _assignees
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION handle_task_roadmap_sort_order(_from_index integer, _to_index integer, _task_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_id uuid;
+BEGIN
+ SELECT project_id INTO _project_id FROM tasks WHERE id = _task_id;
+
+ IF _to_index > _from_index
+ THEN
+ UPDATE tasks
+ SET roadmap_sort_order = roadmap_sort_order - 1
+ WHERE project_id = _project_id
+ AND roadmap_sort_order > _from_index
+ AND roadmap_sort_order <= _to_index;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE tasks
+ SET roadmap_sort_order = roadmap_sort_order + 1
+ WHERE project_id = _project_id
+ AND roadmap_sort_order >= _to_index
+ AND roadmap_sort_order < _from_index;
+ END IF;
+
+ UPDATE tasks SET roadmap_sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION home_task_form_view_model(_user_id uuid, _team_id uuid, _task_id uuid, _project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+BEGIN
+
+ -- Select task info
+ SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ INTO _task
+ FROM (SELECT id,
+ name,
+ start_date,
+ end_date,
+ done,
+ priority_id,
+ project_id,
+ created_at,
+ updated_at,
+ status_id,
+ parent_task_id,
+ parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT COUNT(*) FROM tasks WHERE parent_task_id = _task_id) AS sub_tasks_count,
+ (SELECT name FROM users WHERE id = tasks.reporter_id) AS reporter,
+ (SELECT get_task_assignees(tasks.id)) AS assignees,
+ (SELECT id FROM team_members WHERE user_id = _user_id AND team_id = _team_id) AS team_member_id
+ FROM tasks
+ WHERE id = _task_id) rec;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'task', _task
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION import_tasks_from_template(_project_id uuid, _user_id uuid, _tasks json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _max_sort INT;
+BEGIN
+
+ SELECT COALESCE((SELECT MAX(sort_order) FROM tasks WHERE project_id = _project_id), 0) INTO _max_sort;
+
+ -- insert tasks for task templates
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS(_tasks)
+ LOOP
+ _max_sort = _max_sort + 1;
+ INSERT INTO tasks (name, priority_id, project_id, reporter_id, status_id, sort_order, total_minutes)
+ VALUES (TRIM((_task ->> 'name')::TEXT),
+ (SELECT id FROM task_priorities WHERE value = 1),
+ _project_id,
+ _user_id,
+
+ -- This should be came from client side later
+ (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id::UUID
+ AND category_id IN (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE)
+ LIMIT 1), _max_sort, (_task ->> 'total_minutes')::NUMERIC);
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT('id', _project_id);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION in_organization(_team_id_in uuid, _team_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN (SELECT _team_id_in IN (SELECT id
+ FROM teams
+ WHERE user_id = (SELECT user_id FROM teams WHERE id = _team_id)));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION insert_job_title(_job_title text, _team_id uuid) RETURNS uuid
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _job_title_id UUID;
+BEGIN
+ IF EXISTS(SELECT name FROM job_titles WHERE name = _job_title AND team_id = _team_id)
+ THEN
+ SELECT id FROM job_titles WHERE name = _job_title AND team_id = _team_id INTO _job_title_id;
+ ELSE
+ INSERT INTO job_titles (name, team_id)
+ VALUES (_job_title, _team_id)
+ RETURNING id INTO _job_title_id;
+ END IF;
+
+ RETURN _job_title_id;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION insert_task_list_columns(_project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Key', 'KEY', 0, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Description', 'DESCRIPTION', 2, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Progress', 'PROGRESS', 3, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Members', 'ASSIGNEES', 4, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Labels', 'LABELS', 5, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Status', 'STATUS', 6, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Priority', 'PRIORITY', 7, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Time Tracking', 'TIME_TRACKING', 8, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Estimation', 'ESTIMATION', 9, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Start Date', 'START_DATE', 10, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Due Date', 'DUE_DATE', 11, TRUE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Completed Date', 'COMPLETED_DATE', 12, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Created Date', 'CREATED_DATE', 13, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Last Updated', 'LAST_UPDATED', 14, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Reporter', 'REPORTER', 15, FALSE);
+ INSERT INTO project_task_list_cols (project_id, name, key, index, pinned)
+ VALUES (_project_id, 'Phase', 'PHASE', 16, FALSE);
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_admin(_user_id uuid, _team_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN EXISTS(SELECT 1
+ FROM team_members
+ WHERE team_id = _team_id
+ AND user_id = _user_id
+ AND role_id = (SELECT id
+ FROM roles
+ WHERE id = team_members.role_id
+ AND admin_role IS TRUE));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_completed(_status_id uuid, _project_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN (SELECT _status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id =
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_doing(_status_id uuid, _project_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN (SELECT _status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id =
+ (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE)));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_member_of_project(_project_id uuid, _user_id uuid, _team_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN EXISTS(SELECT 1
+ FROM project_members
+ WHERE project_id = _project_id
+ AND team_member_id = (SELECT id FROM team_members WHERE team_id = _team_id AND user_id = _user_id));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_null_or_empty(_value anyelement) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ RETURN COALESCE(TRIM(_value::TEXT), '') = '';
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION is_owner(_user_id uuid, _team_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN EXISTS(SELECT 1
+ FROM teams
+ WHERE teams.user_id = _user_id
+ AND teams.id = _team_id);
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_todo(_status_id uuid, _project_id uuid) RETURNS boolean
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ RETURN (SELECT _status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = _project_id
+ AND category_id =
+ (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE)));
+END
+$$;
+
+CREATE OR REPLACE FUNCTION lower_email() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+
+ IF (is_null_or_empty(NEW.email) IS FALSE)
+ THEN
+ NEW.email = LOWER(TRIM(NEW.email));
+ END IF;
+
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION migrate_member_allocations(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _member JSON;
+ _output JSON;
+BEGIN
+ FOR _member IN SELECT * FROM JSON_ARRAY_ELEMENTS(_body::JSON)
+ LOOP
+ INSERT INTO project_member_allocations(project_id, team_member_id, allocated_from, allocated_to)
+ VALUES ((_member ->> 'project_id')::UUID, (_member ->> 'team_member_id')::UUID,
+ (_member ->> 'allocated_from')::DATE,
+ (_member ->> 'allocated_to')::DATE);
+ END LOOP;
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION move_tasks_and_delete_status(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _status_id UUID;
+ _category_id UUID;
+BEGIN
+ _status_id = (_body ->> 'id')::UUID;
+ SELECT category_id FROM task_statuses WHERE id = _status_id INTO _category_id;
+
+ IF (is_null_or_empty(_body ->> 'replacing_status') IS FALSE)
+ THEN
+ UPDATE tasks
+ SET status_id = (_body ->> 'replacing_status')::UUID
+ WHERE project_id = (_body ->> 'project_id')::UUID
+ AND status_id = _status_id;
+ END IF;
+
+ IF ((SELECT COUNT(*)
+ FROM task_statuses
+ WHERE category_id = _category_id
+ AND project_id = (_body ->> 'project_id')::UUID) < 2)
+ THEN
+ RAISE 'ERROR_ONE_SHOULD_EXISTS';
+ END IF;
+
+ DELETE FROM task_statuses WHERE id = _status_id AND project_id = (_body ->> 'project_id')::UUID;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION notification_settings_delete_trigger_fn() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ DELETE FROM notification_settings WHERE user_id = OLD.user_id AND team_id = OLD.team_id;
+ RETURN OLD;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION notification_settings_insert_trigger_fn() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+
+ IF (NOT EXISTS(SELECT 1 FROM notification_settings WHERE team_id = NEW.team_id AND user_id = NEW.user_id)) AND
+ (is_null_or_empty(NEW.user_id) IS FALSE) AND (EXISTS(SELECT 1 FROM users WHERE id = NEW.user_id))
+ THEN
+ INSERT INTO notification_settings (popup_notifications_enabled, show_unread_items_count, user_id,
+ team_id)
+ VALUES (TRUE, TRUE, NEW.user_id, NEW.team_id);
+ END IF;
+
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION notify_task_assignment_update(_type text, _reporter_id uuid, _task_id uuid, _user_id uuid, _team_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _reporter_name TEXT;
+ _task_name TEXT;
+ _message TEXT;
+ _team_id UUID;
+ _project_id UUID;
+BEGIN
+ IF (is_null_or_empty(_task_id) IS FALSE)
+ THEN
+
+ SELECT project_id FROM tasks WHERE id = _task_id INTO _project_id;
+ SELECT team_id FROM projects WHERE id = _project_id INTO _team_id;
+
+ INSERT INTO task_updates (type, reporter_id, task_id, user_id, team_id, project_id)
+ VALUES (_type, _reporter_id, _task_id, _user_id, _team_id, (SELECT project_id FROM tasks WHERE id = _task_id));
+
+ SELECT name FROM users WHERE id = _reporter_id INTO _reporter_name;
+ SELECT name FROM tasks WHERE id = _task_id INTO _task_name;
+
+ IF (_type = 'ASSIGN')
+ THEN
+ _message = CONCAT('', _reporter_name, ' has assigned you in ', _task_name, '');
+ ELSE
+ _message = CONCAT('', _reporter_name, ' has removed you from ', _task_name, '');
+ END IF;
+
+ PERFORM create_notification(_user_id, _team_id, _task_id, _project_id, _message);
+
+ RETURN JSON_BUILD_OBJECT(
+ 'receiver_socket_id', (SELECT socket_id FROM users WHERE id = _user_id),
+ 'team', (SELECT name FROM teams WHERE id = _team_id),
+ 'team_id', _team_id,
+ 'project', (SELECT name FROM projects WHERE id = _project_id),
+ 'project_color', (SELECT color_code FROM projects WHERE id = _project_id),
+ 'project_id', _project_id,
+ 'task_id', _task_id,
+ 'message', _message
+ );
+
+ END IF;
+
+ RETURN NULL;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION register_google_user(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _user_id UUID;
+ _organization_id UUID;
+ _team_id UUID;
+ _role_id UUID;
+
+ _name TEXT;
+ _email TEXT;
+ _google_id TEXT;
+BEGIN
+ _name = (_body ->> 'displayName')::TEXT;
+ _email = (_body ->> 'email')::TEXT;
+ _google_id = (_body ->> 'id');
+
+ INSERT INTO users (name, email, google_id, timezone_id)
+ VALUES (_name, _email, _google_id, COALESCE((SELECT id FROM timezones WHERE name = (_body ->> 'timezone')),
+ (SELECT id FROM timezones WHERE name = 'UTC')))
+ RETURNING id INTO _user_id;
+
+ --insert organization data
+ INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
+ trial_expire_date, subscription_status)
+ VALUES (_user_id, TRIM((_body ->> 'displayName')::TEXT), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '14 days',
+ 'trialing')
+ RETURNING id INTO _organization_id;
+
+ INSERT INTO teams (name, user_id, organization_id)
+ VALUES (_name, _user_id, _organization_id)
+ RETURNING id INTO _team_id;
+
+ -- insert default roles
+ INSERT INTO roles (name, team_id, default_role) VALUES ('Member', _team_id, TRUE);
+ INSERT INTO roles (name, team_id, admin_role) VALUES ('Admin', _team_id, TRUE);
+ INSERT INTO roles (name, team_id, owner) VALUES ('Owner', _team_id, TRUE) RETURNING id INTO _role_id;
+
+ INSERT INTO team_members (user_id, team_id, role_id)
+ VALUES (_user_id, _team_id, _role_id);
+
+ IF (is_null_or_empty(_body ->> 'team') OR is_null_or_empty(_body ->> 'member_id'))
+ THEN
+ UPDATE users SET active_team = _team_id WHERE id = _user_id;
+ ELSE
+ -- Verify team member
+ IF EXISTS(SELECT id
+ FROM team_members
+ WHERE id = (_body ->> 'member_id')::UUID
+ AND team_id = (_body ->> 'team')::UUID)
+ THEN
+ UPDATE team_members
+ SET user_id = _user_id
+ WHERE id = (_body ->> 'member_id')::UUID
+ AND team_id = (_body ->> 'team')::UUID;
+
+ DELETE
+ FROM email_invitations
+ WHERE team_id = (_body ->> 'team')::UUID
+ AND team_member_id = (_body ->> 'member_id')::UUID;
+
+ UPDATE users SET active_team = (_body ->> 'team')::UUID WHERE id = _user_id;
+ END IF;
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _user_id,
+ 'email', _email,
+ 'google_id', _google_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION register_user(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _user_id UUID;
+ _organization_id UUID;
+ _team_id UUID;
+ _role_id UUID;
+ _trimmed_email TEXT;
+ _trimmed_name TEXT;
+ _trimmed_team_name TEXT;
+BEGIN
+
+ _trimmed_email = LOWER(TRIM((_body ->> 'email')));
+ _trimmed_name = TRIM((_body ->> 'name'));
+ _trimmed_team_name = TRIM((_body ->> 'team_name'));
+
+ -- check user exists
+ IF EXISTS(SELECT email FROM users WHERE email = _trimmed_email)
+ THEN
+ RAISE 'EMAIL_EXISTS_ERROR:%', (_body ->> 'email');
+ END IF;
+
+ -- insert user
+ INSERT INTO users (name, email, password, timezone_id)
+ VALUES (_trimmed_name, _trimmed_email, (_body ->> 'password'),
+ COALESCE((SELECT id FROM timezones WHERE name = (_body ->> 'timezone')),
+ (SELECT id FROM timezones WHERE name = 'UTC')))
+ RETURNING id INTO _user_id;
+
+ --insert organization data
+ INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
+ trial_expire_date, subscription_status)
+ VALUES (_user_id, TRIM((_body ->> 'team_name')::TEXT), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '14 days',
+ 'trialing')
+ RETURNING id INTO _organization_id;
+
+ -- insert team
+ INSERT INTO teams (name, user_id, organization_id)
+ VALUES (_trimmed_team_name, _user_id, _organization_id)
+ RETURNING id INTO _team_id;
+
+ IF (is_null_or_empty((_body ->> 'invited_team_id')))
+ THEN
+ UPDATE users SET active_team = _team_id WHERE id = _user_id;
+ ELSE
+ IF NOT EXISTS(SELECT id
+ FROM email_invitations
+ WHERE team_id = (_body ->> 'invited_team_id')::UUID
+ AND email = _trimmed_email)
+ THEN
+ RAISE 'ERROR_INVALID_JOINING_EMAIL';
+ END IF;
+ UPDATE users SET active_team = (_body ->> 'invited_team_id')::UUID WHERE id = _user_id;
+ END IF;
+
+ -- insert default roles
+ INSERT INTO roles (name, team_id, default_role) VALUES ('Member', _team_id, TRUE);
+ INSERT INTO roles (name, team_id, admin_role) VALUES ('Admin', _team_id, TRUE);
+ INSERT INTO roles (name, team_id, owner) VALUES ('Owner', _team_id, TRUE) RETURNING id INTO _role_id;
+
+ -- insert team member
+ INSERT INTO team_members (user_id, team_id, role_id)
+ VALUES (_user_id, _team_id, _role_id);
+
+ -- update team member table with user id
+ IF (_body ->> 'team_member_id') IS NOT NULL
+ THEN
+ UPDATE team_members SET user_id = (_user_id)::UUID WHERE id = (_body ->> 'team_member_id')::UUID;
+ DELETE
+ FROM email_invitations
+ WHERE email = _trimmed_email
+ AND team_member_id = (_body ->> 'team_member_id')::UUID;
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _user_id,
+ 'name', _trimmed_name,
+ 'email', _trimmed_email,
+ 'team_id', _team_id
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION remove_project_member(_id uuid, _user_id uuid, _team_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _team_member_id UUID;
+ _project_id UUID;
+ _member_user_id UUID;
+ _notification TEXT;
+BEGIN
+ SELECT project_id FROM project_members WHERE id = _id INTO _project_id;
+ SELECT team_member_id FROM project_members WHERE id = _id INTO _team_member_id;
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _member_user_id;
+ DELETE FROM project_members WHERE id = _id;
+ DELETE FROM project_member_allocations WHERE project_id = _project_id AND team_member_id = _team_member_id;
+
+ IF (_member_user_id != _user_id)
+ THEN
+ _notification =
+ CONCAT('You have been removed from the ', (SELECT name FROM projects WHERE id = _project_id),
+ ' by ',
+ (SELECT name FROM users WHERE id = _user_id), '');
+ PERFORM create_notification(
+ (SELECT user_id FROM team_members WHERE id = _team_member_id),
+ _team_id,
+ NULL,
+ _project_id,
+ _notification
+ );
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _id,
+ 'notification', _notification,
+ 'socket_id', (SELECT socket_id FROM users WHERE id = _member_user_id),
+ 'project', (SELECT name FROM projects WHERE id = _project_id),
+ 'project_id', _project_id,
+ 'project_color', (SELECT color_code FROM projects WHERE id = _project_id),
+ 'team', (SELECT name FROM teams WHERE id = _team_id),
+ 'member_user_id', _member_user_id
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION remove_task_assignee(_task_id uuid, _team_member_id uuid, _project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _team_id UUID;
+ _user_id UUID;
+BEGIN
+
+ SELECT team_id FROM team_members WHERE id = _team_member_id INTO _team_id;
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _user_id;
+
+ DELETE
+ FROM tasks_assignees
+ WHERE task_id = _task_id
+ AND project_member_id =
+ (SELECT id FROM project_members WHERE team_member_id = _team_member_id AND project_id = _project_id);
+
+ RETURN JSON_BUILD_OBJECT(
+ 'user_id', _user_id,
+ 'team_id', _team_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION remove_team_member(_id uuid, _user_id uuid, _team_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _removed_user_id UUID;
+ _removed_team_name TEXT;
+BEGIN
+ SELECT user_id FROM team_members WHERE id = _id INTO _removed_user_id;
+ SELECT name FROM teams WHERE id = _team_id INTO _removed_team_name;
+
+ UPDATE users
+ SET active_team = (SELECT id FROM teams WHERE user_id = _removed_user_id LIMIT 1)
+ WHERE active_team = _team_id
+ AND id = _removed_user_id;
+
+ PERFORM create_notification(
+ _removed_user_id,
+ _team_id,
+ NULL,
+ NULL,
+ CONCAT('You have been removed from ', (SELECT name FROM teams WHERE id = _team_id), ' by ',
+ (SELECT name FROM users WHERE id = _user_id), '')
+ );
+
+ DELETE FROM team_members WHERE id = _id AND team_id = _team_id;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _removed_user_id,
+ 'team', _removed_team_name,
+ 'socket_id', (SELECT socket_id FROM users WHERE id = _user_id)
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION resend_team_invitation(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _id UUID;
+ _team_id UUID;
+ _user_id UUID;
+ _email TEXT;
+ _output JSON;
+BEGIN
+ _team_id = (_body ->> 'team_id')::UUID;
+ _id = (_body ->> 'id')::UUID;
+
+ SELECT email FROM email_invitations WHERE team_member_id = _id AND team_id = _team_id INTO _email;
+ SELECT id FROM users WHERE email = _email INTO _user_id;
+
+ IF is_null_or_empty(_email) IS FALSE
+ THEN
+ DELETE FROM email_invitations WHERE team_id = _team_id AND team_member_id = _id;
+ END IF;
+
+ INSERT INTO email_invitations(team_id, team_member_id, email, name)
+ VALUES (_team_id, _id, _email, SPLIT_PART(_email, '@', 1));
+
+ SELECT JSON_BUILD_OBJECT(
+ 'name', (SELECT name FROM users WHERE id = _user_id),
+ 'email', _email,
+ 'is_new', is_null_or_empty(_user_id),
+ 'team_member_id', _id,
+ 'team_member_user_id', _user_id
+ )
+ INTO _output;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION set_active_team(_user_id uuid, _team_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF EXISTS(SELECT id FROM team_members WHERE team_id = _team_id AND user_id = _user_id)
+ THEN
+ UPDATE users SET active_team = _team_id WHERE id = _user_id;
+ ELSE
+ UPDATE users SET active_team = (SELECT id FROM teams WHERE user_id = users.id LIMIT 1) WHERE id = _user_id;
+ END IF;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION set_active_team_by_member_id(_team_member_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ PERFORM set_active_team(
+ (SELECT user_id FROM team_members WHERE id = _team_member_id),
+ (SELECT team_id FROM team_members WHERE id = _team_member_id)
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION set_task_updated_at_trigger_fn() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION slugify(value text) RETURNS text
+ IMMUTABLE
+ STRICT
+ LANGUAGE sql
+AS
+$$
+ -- removes accents (diacritic signs) from a given string --
+WITH "unaccented" AS (SELECT unaccent("value") AS "value"),
+ -- lowercase the string
+ "lowercase" AS (SELECT LOWER("value") AS "value"
+ FROM "unaccented"),
+ -- replaces anything that's not a letter, number, hyphen('-'), or underscore('_') with a hyphen('-')
+ "hyphenated" AS (SELECT REGEXP_REPLACE("value", '[^a-z0-9\\-_.''`"]+', '-', 'gi') AS "value"
+ FROM "lowercase"),
+ -- trims hyphens('-') if they exist on the head or tail of the string
+ "trimmed" AS (SELECT REGEXP_REPLACE(REGEXP_REPLACE("value", '\\-+$', ''), '^\\-', '') AS "value"
+ FROM "hyphenated")
+SELECT "value"
+FROM "trimmed";
+$$;
+
+CREATE OR REPLACE FUNCTION sys_insert_project_healths() RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Not Set', '#a9a9a9', 0, TRUE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Needs Attention', '#fbc84c', 1, FALSE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('At Risk', '#f37070', 2, FALSE);
+ INSERT INTO sys_project_healths (name, color_code, sort_order, is_default)
+ VALUES ('Good', '#75c997', 3, FALSE);
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION task_status_change_trigger_fn() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF ((SELECT is_done
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = NEW.status_id)) IS TRUE)
+ THEN
+ UPDATE tasks SET completed_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
+ ELSE
+ UPDATE tasks SET completed_at = NULL WHERE id = NEW.id;
+ END IF;
+
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION tasks_task_subscriber_notify_done_trigger() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF (EXISTS(SELECT 1
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = NEW.status_id)
+ AND is_done IS TRUE))
+ THEN
+ PERFORM pg_notify('db_task_status_changed', NEW.id::TEXT);
+ END IF;
+
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION to_seconds(t text) RETURNS integer
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ hs INTEGER;
+ ms INTEGER;
+ s INTEGER;
+BEGIN
+ SELECT (EXTRACT(HOUR FROM t::TIME) * 60 * 60) INTO hs;
+ SELECT (EXTRACT(MINUTES FROM t::TIME) * 60) INTO ms;
+ SELECT (EXTRACT(SECONDS FROM t::TIME)) INTO s;
+ SELECT (hs + ms + s) INTO s;
+ RETURN s;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION toggle_archive_all_projects(_project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _user_id UUID;
+
+BEGIN
+ IF EXISTS(SELECT project_id FROM archived_projects WHERE project_id = _project_id)
+ THEN
+ DELETE FROM archived_projects WHERE project_id = _project_id;
+ ELSE
+ FOR _user_id IN
+ SELECT user_id FROM team_members WHERE team_id = (SELECT team_id FROM projects WHERE id = _project_id)
+ LOOP
+ IF NOT (is_null_or_empty(_user_id))
+ THEN
+ INSERT INTO archived_projects (user_id, project_id)
+ VALUES (_user_id, _project_id);
+ END IF;
+ END LOOP;
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION toggle_archive_project(_user_id uuid, _project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF EXISTS(SELECT user_id FROM archived_projects WHERE user_id = _user_id AND project_id = _project_id)
+ THEN
+ DELETE FROM archived_projects WHERE user_id = _user_id AND project_id = _project_id;
+ ELSE
+ INSERT INTO archived_projects (user_id, project_id) VALUES (_user_id, _project_id);
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION toggle_favorite_project(_user_id uuid, _project_id uuid) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ IF EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = _user_id AND project_id = _project_id)
+ THEN
+ DELETE FROM favorite_projects WHERE user_id = _user_id AND project_id = _project_id;
+ ELSE
+ INSERT INTO favorite_projects (user_id, project_id) VALUES (_user_id, _project_id);
+ END IF;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION update_project(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _user_id UUID;
+ _team_id UUID;
+ _client_id UUID;
+ _project_id UUID;
+ _project_manager_team_member_id UUID;
+ _client_name TEXT;
+ _project_name TEXT;
+BEGIN
+ -- need a test, can be throw errors
+ _client_name = TRIM((_body ->> 'client_name')::TEXT);
+ _project_name = TRIM((_body ->> 'name')::TEXT);
+
+ -- add inside the controller
+ _user_id = (_body ->> 'user_id')::UUID;
+ _team_id = (_body ->> 'team_id')::UUID;
+ _project_manager_team_member_id = (_body ->> 'team_member_id')::UUID;
+
+ -- cache exists client if exists
+ SELECT id FROM clients WHERE LOWER(name) = LOWER(_client_name) AND team_id = _team_id INTO _client_id;
+
+ -- insert client if not exists
+ IF is_null_or_empty(_client_id) IS TRUE AND is_null_or_empty(_client_name) IS FALSE
+ THEN
+ INSERT INTO clients (name, team_id) VALUES (_client_name, _team_id) RETURNING id INTO _client_id;
+ END IF;
+
+ -- check whether the project name is already in
+ IF EXISTS(
+ SELECT name FROM projects WHERE LOWER(name) = LOWER(_project_name)
+ AND team_id = _team_id AND id != (_body ->> 'id')::UUID
+ )
+ THEN
+ RAISE 'PROJECT_EXISTS_ERROR:%', _project_name;
+ END IF;
+
+ -- update the project
+ UPDATE projects
+ SET name = _project_name,
+ notes = (_body ->> 'notes')::TEXT,
+ color_code = (_body ->> 'color_code')::TEXT,
+ status_id = (_body ->> 'status_id')::UUID,
+ health_id = (_body ->> 'health_id')::UUID,
+ key = (_body ->> 'key')::TEXT,
+ start_date = (_body ->> 'start_date')::TIMESTAMPTZ,
+ end_date = (_body ->> 'end_date')::TIMESTAMPTZ,
+ client_id = _client_id,
+ folder_id = (_body ->> 'folder_id')::UUID,
+ category_id = (_body ->> 'category_id')::UUID,
+ updated_at = CURRENT_TIMESTAMP,
+ estimated_working_days = (_body ->> 'working_days')::INTEGER,
+ estimated_man_days = (_body ->> 'man_days')::INTEGER,
+ hours_per_day = (_body ->> 'hours_per_day')::INTEGER
+ WHERE id = (_body ->> 'id')::UUID
+ AND team_id = _team_id
+ RETURNING id INTO _project_id;
+
+ UPDATE project_members SET project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'MEMBER') WHERE project_id = _project_id;
+
+ IF NOT (_project_manager_team_member_id IS NULL)
+ THEN
+ PERFORM update_project_manager(_project_manager_team_member_id, _project_id::UUID);
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _project_id,
+ 'name', (_body ->> 'name')::TEXT,
+ 'project_manager_id', _project_manager_team_member_id::UUID
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_project_manager(_team_member_id uuid, _project_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _project_member_id UUID;
+ _team_id UUID;
+ _user_id UUID;
+ _project_member JSON;
+BEGIN
+
+ SELECT id
+ FROM project_members
+ WHERE team_member_id = _team_member_id
+ AND project_id = _project_id
+ INTO _project_member_id;
+
+ SELECT team_id FROM team_members WHERE id = _team_member_id INTO _team_id;
+ SELECT user_id FROM team_members WHERE id = _team_member_id INTO _user_id;
+
+ IF is_null_or_empty(_project_member_id)
+ THEN
+ SELECT create_project_member(JSON_BUILD_OBJECT(
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'project_id', _project_id,
+ 'user_id', _user_id,
+ 'access_level', 'PROJECT_MANAGER'::TEXT
+ ))
+ INTO _project_member;
+ END IF;
+
+ UPDATE project_members SET project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER') WHERE id = _project_member_id AND project_id = _project_id;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'project_member_id', _project_member_id,
+ 'team_member_id', _team_member_id,
+ 'team_id', _team_id,
+ 'user_id', _user_id
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION update_project_tasks_counter_trigger_fn() RETURNS trigger
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+
+ UPDATE projects SET tasks_counter = (tasks_counter + 1) WHERE id = NEW.project_id;
+ NEW.task_no = (SELECT tasks_counter FROM projects WHERE id = NEW.project_id);
+
+ RETURN NEW;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION update_status_order(_status_ids json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _iterator NUMERIC := 0;
+ _status_id TEXT;
+BEGIN
+
+ FOR _status_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_status_ids)::JSON)
+ LOOP
+ UPDATE task_statuses
+ SET sort_order = _iterator
+ WHERE id = (SELECT TRIM(BOTH '"' FROM _status_id))::UUID;
+ _iterator := _iterator + 1;
+ END LOOP;
+
+ RETURN;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION update_task(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _assignee TEXT;
+ _assignee_id UUID;
+ _label JSON;
+ _old_assignees JSON;
+ _new_assignees JSON;
+BEGIN
+ UPDATE tasks
+ SET name = TRIM((_body ->> 'name')::TEXT),
+ start_date = (_body ->> 'start')::TIMESTAMPTZ,
+ end_date = (_body ->> 'end')::TIMESTAMPTZ,
+ priority_id = (_body ->> 'priority_id')::UUID,
+ description = COALESCE(TRIM((_body ->> 'description')::TEXT), description),
+ total_minutes = (_body ->> 'total_minutes')::NUMERIC,
+ status_id = (_body ->> 'status_id')::UUID
+ WHERE id = (_body ->> 'id')::UUID;
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _old_assignees
+ FROM (
+ --
+ SELECT team_member_id,
+ (SELECT user_id FROM team_members WHERE id = tasks_assignees.team_member_id),
+ (SELECT team_id FROM team_members WHERE id = tasks_assignees.team_member_id)
+ FROM tasks_assignees
+ WHERE task_id = (_body ->> 'id')::UUID
+ --
+ ) rec;
+
+ -- delete existing task assignees
+ DELETE FROM tasks_assignees WHERE task_id = (_body ->> 'id')::UUID;
+
+ -- insert task assignees
+ FOR _assignee IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'assignees')::JSON)
+ LOOP
+ _assignee_id = TRIM('"' FROM _assignee)::UUID;
+ PERFORM create_task_assignee(_assignee_id, (_body ->> 'project_id')::UUID, (_body ->> 'id')::UUID,
+ (_body ->> 'reporter_id')::UUID);
+ END LOOP;
+
+ IF ((_body ->> 'inline')::BOOLEAN IS FALSE)
+ THEN
+ DELETE FROM task_labels WHERE task_id = (_body ->> 'id')::UUID;
+ FOR _label IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'labels')::JSON)
+ LOOP
+ PERFORM assign_or_create_label((_body ->> 'team_id')::UUID, (_body ->> 'id')::UUID,
+ (_label ->> 'name')::TEXT,
+ (_label ->> 'color')::TEXT);
+ END LOOP;
+ END IF;
+
+
+ SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ INTO _new_assignees
+ FROM (
+ --
+ SELECT team_member_id,
+ (SELECT user_id FROM team_members WHERE id = tasks_assignees.team_member_id),
+ (SELECT team_id FROM team_members WHERE id = tasks_assignees.team_member_id)
+ FROM tasks_assignees
+ WHERE task_id = (_body ->> 'id')::UUID
+ --
+ ) rec;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', (_body ->> 'id')::UUID,
+ 'name', (_body ->> 'name')::TEXT,
+ 'old_assignees', _old_assignees,
+ 'new_assignees', _new_assignees
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_task_status(_updated_task_id uuid, _status_id uuid, _task_ids json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _iterator NUMERIC := 0;
+ _task_id TEXT;
+BEGIN
+ UPDATE tasks SET status_id = _status_id WHERE id = _updated_task_id;
+
+ FOR _task_id IN SELECT * FROM JSON_ARRAY_ELEMENTS((_task_ids)::JSON)
+ LOOP
+ UPDATE tasks
+ SET sort_order = _iterator
+ WHERE id = (SELECT TRIM(BOTH '"' FROM _task_id))::UUID;
+ _iterator := _iterator + 1;
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _updated_task_id
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_task_status(_task_id uuid, _project_id uuid, _status_id uuid, _from_index integer, _to_index integer) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+BEGIN
+ UPDATE tasks SET status_id = _status_id WHERE id = _task_id AND project_id = _project_id;
+
+ IF (_from_index != _to_index)
+ THEN
+ IF _to_index > _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order - 1
+ WHERE project_id = _project_id
+ AND sort_order > _from_index
+ AND sort_order <= _to_index;
+ END IF;
+
+ IF _to_index < _from_index
+ THEN
+ UPDATE tasks
+ SET sort_order = sort_order + 1
+ WHERE project_id = _project_id
+ AND sort_order >= _to_index
+ AND sort_order < _from_index;
+ END IF;
+
+ UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
+ END IF;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _task_id
+ );
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_task_template(_id uuid, _name text, _tasks json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+
+BEGIN
+ UPDATE task_templates SET name = _name, updated_at = NOW() WHERE id = _id;
+
+ -- delete all existing tasks for the selected template
+ DELETE FROM task_templates_tasks WHERE template_id = _id;
+
+ -- insert tasks for task templates
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS(_tasks)
+ LOOP
+ INSERT INTO task_templates_tasks (template_id, name) VALUES (_id, (_task ->> 'name')::TEXT);
+ END LOOP;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'id', _id,
+ 'template_name', _name
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION update_team_member(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _team_id UUID;
+ _job_title_id UUID;
+ _role_id UUID;
+BEGIN
+ _team_id = (_body ->> 'team_id')::UUID;
+
+ IF ((_body ->> 'is_admin')::BOOLEAN IS TRUE)
+ THEN
+ SELECT id FROM roles WHERE admin_role IS TRUE INTO _role_id;
+ ELSE
+ SELECT id FROM roles WHERE default_role IS TRUE INTO _role_id;
+ END IF;
+
+ IF is_null_or_empty((_body ->> 'job_title')) IS FALSE
+ THEN
+ SELECT insert_job_title((_body ->> 'job_title')::TEXT, _team_id) INTO _job_title_id;
+ ELSE
+ _job_title_id = NULL;
+ END IF;
+
+ UPDATE team_members
+ SET job_title_id = _job_title_id,
+ role_id = _role_id,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = (_body ->> 'id')::UUID
+ AND team_id = _team_id;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_team_name(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _name TEXT;
+BEGIN
+ _name = (_body ->> 'name')::TEXT;
+
+ IF ((SELECT name FROM teams WHERE id = (_body ->> 'id')::UUID) != _name)
+ THEN
+ UPDATE teams
+ SET name = _name,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = (_body ->> 'id')::UUID
+ AND user_id = (_body ->> 'user_id')::UUID;
+ END IF;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_existing_phase_colors(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ result JSON;
+ _phase JSON;
+BEGIN
+
+ FOR _phase IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'phases')::JSON)
+ LOOP
+ UPDATE project_phases SET color_code = (_phase ->> 'color_code')::WL_HEX_COLOR WHERE id = (_phase ->> 'id')::UUID;
+ END LOOP;
+
+ SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec)))
+ FROM (SELECT * FROM project_phases) rec
+ INTO result;
+
+ RETURN result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_existing_phase_sort_order(_body json) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ result JSON;
+ _phase JSON;
+BEGIN
+
+ FOR _phase IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'phases')::JSON)
+ LOOP
+ UPDATE project_phases SET sort_index = (_phase ->> 'sort_number')::INT WHERE id = (_phase ->> 'id')::UUID AND project_id = (_phase ->> 'project_id')::UUID;
+ END LOOP;
+
+ SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec)))
+ FROM (SELECT * FROM project_phases) rec
+ INTO result;
+
+ RETURN result;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION handle_phase_sort_order(_body json) RETURNS void
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _phase JSON;
+ _sort_index INT := 0;
+BEGIN
+
+ FOR _phase IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'phases')::JSON)
+ LOOP
+ UPDATE project_phases SET sort_index = _sort_index::INT WHERE id = (_phase ->> 'id')::UUID AND project_id = (_body ->> 'project_id')::UUID;
+ _sort_index = _sort_index + 1;
+ END LOOP;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_change_tasks_status(_body json, _userid uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+ _previous_status UUID;
+BEGIN
+
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ _previous_status = (SELECT status_id FROM tasks WHERE id = (_task ->> 'id')::UUID);
+
+ UPDATE tasks SET status_id = (_body ->> 'status_id')::UUID WHERE id = (_task ->> 'id')::UUID;
+
+ IF (_previous_status IS DISTINCT FROM (_body ->> 'status_id')::UUID)
+ THEN
+ INSERT INTO task_activity_logs (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, project_id)
+ VALUES (
+ (_task ->> 'id')::UUID,
+ (SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)),
+ 'status',
+ _userId,
+ 'update',
+ _previous_status,
+ (_body ->> 'status_id')::UUID,
+ (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)
+ );
+ END IF;
+ END LOOP;
+ RETURN _output;
+END
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_change_tasks_priority(_body json, _userid uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+ _previous_priority UUID;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ _previous_priority = (SELECT priority_id FROM tasks WHERE id = (_task ->> 'id')::UUID);
+
+ UPDATE tasks SET priority_id = (_body ->> 'priority_id')::UUID WHERE id = (_task ->> 'id')::UUID;
+
+ IF (_previous_priority IS DISTINCT FROM (_body ->> 'priority_id')::UUID)
+ THEN
+ INSERT INTO task_activity_logs (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, project_id)
+ VALUES (
+ (_task ->> 'id')::UUID,
+ (SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)),
+ 'priority',
+ _userId,
+ 'update',
+ _previous_priority,
+ (_body ->> 'priority_id')::UUID,
+ (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)
+ );
+ END IF;
+ END LOOP;
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_change_tasks_phase(_body json, _userid uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _output JSON;
+ _previous_phase UUID;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ _previous_phase = (SELECT phase_id FROM task_phase WHERE task_id = (_task ->> 'id')::UUID);
+
+ IF NOT EXISTS(SELECT 1 FROM task_phase WHERE task_id = (_task ->> 'id')::UUID)
+ THEN
+ INSERT INTO task_phase (task_id, phase_id) VALUES ((_task ->> 'id')::UUID, (_body ->> 'phase_id')::UUID);
+ ELSE
+ UPDATE task_phase SET phase_id = (_body ->> 'phase_id')::UUID WHERE task_id = (_task ->> 'id')::UUID;
+ END IF;
+
+ IF (_previous_phase IS DISTINCT FROM (_body ->> 'phase_id')::UUID)
+ THEN
+ INSERT INTO task_activity_logs (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, project_id)
+ VALUES (
+ (_task ->> 'id')::UUID,
+ (SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)),
+ 'phase',
+ _userId,
+ 'update',
+ _previous_phase,
+ (_body ->> 'phase_id')::UUID,
+ (SELECT project_id FROM tasks WHERE id = (_task ->> 'id')::UUID)
+ );
+ END IF;
+
+
+ END LOOP;
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION bulk_assign_label(_body json, _user_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _task JSON;
+ _label JSON;
+ _output JSON;
+BEGIN
+ FOR _task IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'tasks')::JSON)
+ LOOP
+ FOR _label IN SELECT * FROM JSON_ARRAY_ELEMENTS((_body ->> 'labels')::JSON)
+ LOOP
+
+ DELETE FROM task_labels WHERE task_id = (_task ->> 'id')::UUID AND label_id = (_label ->> 'id')::UUID;
+
+ INSERT INTO task_labels (task_id, label_id) VALUES ((_task ->> 'id')::UUID, (_label ->> 'id')::UUID);
+
+ INSERT INTO task_activity_logs
+ (task_id, team_id, attribute_type, user_id, log_type, old_value, new_value, next_string, project_id)
+ VALUES
+ (
+ (_task ->> 'id')::UUID,
+ (SELECT team_id FROM projects WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = (_task ->> 'id')::UUID)),
+ 'label',
+ _user_id::UUID,
+ 'create',
+ NULL,
+ (_label ->> 'id')::UUID,
+ NULL,
+ (SELECT project_id FROM tasks WHERE tasks.id = (_task ->> 'id')::UUID)
+ );
+
+ END LOOP;
+ END LOOP;
+
+ RETURN _output;
+END;
+$$;
+
+CREATE OR REPLACE FUNCTION update_phase_name(_phase_id uuid, _phase_name text, _template_id uuid) RETURNS json
+ LANGUAGE plpgsql
+AS
+$$
+DECLARE
+ _color_code TEXT;
+BEGIN
+ IF EXISTS(SELECT name
+ FROM cpt_phases
+ WHERE name = _phase_name
+ AND template_id = _template_id)
+ THEN
+ RAISE 'PHASE_EXISTS_ERROR:%', _phase_name::TEXT;
+ END IF;
+ UPDATE cpt_phases
+ SET name = _phase_name
+ WHERE id = _phase_id
+ AND template_id = _template_id
+ RETURNING color_code INTO _color_code;
+
+ RETURN JSON_BUILD_OBJECT(
+ 'color_code', _color_code,
+ 'id', _phase_id,
+ 'name', _phase_name
+ );
+END
+$$;
+
+CREATE OR REPLACE FUNCTION is_overdue_for_date(_task_id UUID, _end_date DATE) RETURNS BOOL AS
+$$
+DECLARE
+BEGIN
+ RETURN EXISTS(SELECT 1
+ FROM tasks
+ WHERE id = _task_id
+ AND end_date < _end_date
+ AND is_completed(tasks.status_id, tasks.project_id) IS FALSE);
+END
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION is_overdue(_task_id UUID) RETURNS BOOL AS
+$$
+DECLARE
+BEGIN
+ RETURN EXISTS(SELECT 1
+ FROM tasks
+ WHERE id = _task_id
+ AND end_date < CURRENT_TIMESTAMP
+ AND is_completed(tasks.status_id, tasks.project_id) IS FALSE);
+END
+$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION is_completed_between(_task_id UUID, _start_date DATE, _end_date DATE) RETURNS BOOL AS
+$$
+DECLARE
+ BEGIN
+ RETURN EXISTS ( SELECT 1 FROM tasks WHERE id = _task_id AND completed_at::DATE >= _start_date::DATE AND completed_at::DATE <= _end_date::DATE);
+END
+$$ LANGUAGE plpgsql;
diff --git a/worklenz-backend/database/6_user-permission.sql b/worklenz-backend/database/6_user-permission.sql
new file mode 100644
index 00000000..c4078bae
--- /dev/null
+++ b/worklenz-backend/database/6_user-permission.sql
@@ -0,0 +1,31 @@
+REVOKE CREATE ON SCHEMA public FROM PUBLIC;
+CREATE ROLE worklenz_client;
+
+GRANT CONNECT ON DATABASE "DATABASE_NAME" TO worklenz_client;
+GRANT INSERT, SELECT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO worklenz_client;
+
+GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON task_priorities FROM worklenz_client;
+GRANT SELECT ON task_priorities TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON project_access_levels FROM worklenz_client;
+GRANT SELECT ON project_access_levels TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON timezones FROM worklenz_client;
+GRANT SELECT ON timezones TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON worklenz_alerts FROM worklenz_client;
+GRANT SELECT ON worklenz_alerts TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON sys_task_status_categories FROM worklenz_client;
+GRANT SELECT ON sys_task_status_categories TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON sys_project_statuses FROM worklenz_client;
+GRANT SELECT ON sys_project_statuses TO worklenz_client;
+
+REVOKE ALL PRIVILEGES ON sys_project_healths FROM worklenz_client;
+GRANT SELECT ON sys_project_healths TO worklenz_client;
+
+CREATE USER worklenz_backend WITH PASSWORD 'PASSWORD';
+GRANT worklenz_client TO worklenz_backend;
diff --git a/worklenz-backend/database/README.md b/worklenz-backend/database/README.md
new file mode 100644
index 00000000..f616d601
--- /dev/null
+++ b/worklenz-backend/database/README.md
@@ -0,0 +1 @@
+All database DDLs, DMLs and migrations relates to the application should be stored here as well.
diff --git a/worklenz-backend/doc/Database.md b/worklenz-backend/doc/Database.md
new file mode 100644
index 00000000..96243176
--- /dev/null
+++ b/worklenz-backend/doc/Database.md
@@ -0,0 +1,3 @@
+- When you selecting user's info, like name, email, avatar etc. Use `team_member_info_view`
+- System default tables should be prefixed with `sys_`. (e.g. `sys_priorities`)
+- Tasks should be queried by skipping archived tasks
diff --git a/worklenz-backend/esbuild.js b/worklenz-backend/esbuild.js
new file mode 100644
index 00000000..47b422e2
--- /dev/null
+++ b/worklenz-backend/esbuild.js
@@ -0,0 +1,38 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
+
+// still working on this...
+
+const esbuild = require("esbuild");
+const fs = require("fs");
+const path = require("path");
+
+function getTsFiles(directoryPath) {
+ const files = fs.readdirSync(directoryPath);
+
+ let tsFiles = [];
+
+ files.forEach(file => {
+ const filePath = path.join(directoryPath, file);
+ const fileStat = fs.statSync(filePath);
+
+ if (fileStat.isFile() && path.extname(file) === ".ts") {
+ tsFiles.push(filePath);
+ } else if (fileStat.isDirectory()) {
+ const subdirectoryTsFiles = getTsFiles(filePath);
+ tsFiles = tsFiles.concat(subdirectoryTsFiles);
+ }
+ });
+
+ return tsFiles;
+}
+
+esbuild.build({
+ entryPoints: getTsFiles("src"),
+ platform: "node",
+ minify: false,
+ target: "esnext",
+ format: "cjs",
+ tsconfig: "tsconfig.prod.json",
+ outdir: "build",
+ logLevel: "debug"
+});
diff --git a/worklenz-backend/grunt/grunt-compress.js b/worklenz-backend/grunt/grunt-compress.js
new file mode 100644
index 00000000..b903edf6
--- /dev/null
+++ b/worklenz-backend/grunt/grunt-compress.js
@@ -0,0 +1,28 @@
+module.exports = {
+ brotli_js: {
+ options: {
+ mode: "brotli",
+ brotli: {
+ mode: 1
+ }
+ },
+ expand: true,
+ cwd: "build/public",
+ src: ["**/*.js"],
+ dest: "build/public",
+ extDot: "last",
+ ext: ".js.br"
+ },
+ gzip_js: {
+ options: {
+ mode: "gzip"
+ },
+ files: [{
+ expand: true,
+ cwd: "build/public",
+ src: ["**/*.js"],
+ dest: "build/public",
+ ext: ".js.gz"
+ }]
+ }
+};
diff --git a/worklenz-backend/jest.config.js b/worklenz-backend/jest.config.js
new file mode 100644
index 00000000..43800186
--- /dev/null
+++ b/worklenz-backend/jest.config.js
@@ -0,0 +1,196 @@
+/*
+ * For a detailed explanation regarding each configuration property, visit:
+ * https://jestjs.io/docs/configuration
+ */
+
+module.exports = {
+ // All imported modules in your tests should be mocked automatically
+ automock: true,
+
+ // Stop running tests after `n` failures
+ // bail: 0,
+
+ // The directory where Jest should store its cached dependency information
+ // cacheDirectory: "C:\\Users\\dinindu\\AppData\\Local\\Temp\\jest",
+
+ // Automatically clear mock calls and instances between every test
+ clearMocks: true,
+
+ // Indicates whether the coverage information should be collected while executing the test
+ collectCoverage: true,
+
+ // An array of glob patterns indicating a set of files for which coverage information should be collected
+ // collectCoverageFrom: undefined,
+
+ // The directory where Jest should output its coverage files
+ coverageDirectory: "coverage",
+
+ // An array of regexp pattern strings used to skip coverage collection
+ coveragePathIgnorePatterns: [
+ "\\\\node_modules\\\\"
+ ],
+
+ // Indicates which provider should be used to instrument code for coverage
+ // coverageProvider: "babel",
+
+ // A list of reporter names that Jest uses when writing coverage reports
+ // coverageReporters: [
+ // "json",
+ // "text",
+ // "lcov",
+ // "clover"
+ // ],
+
+ // An object that configures minimum threshold enforcement for coverage results
+ // coverageThreshold: undefined,
+
+ // A path to a custom dependency extractor
+ // dependencyExtractor: undefined,
+
+ // Make calling deprecated APIs throw helpful error messages
+ // errorOnDeprecated: false,
+
+ // Force coverage collection from ignored files using an array of glob patterns
+ // forceCoverageMatch: [],
+
+ // A path to a module which exports an async function that is triggered once before all test suites
+ // globalSetup: undefined,
+
+ // A path to a module which exports an async function that is triggered once after all test suites
+ // globalTeardown: undefined,
+
+ // A set of global variables that need to be available in all test environments
+ // globals: {},
+
+ // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
+ // maxWorkers: "50%",
+
+ // An array of directory names to be searched recursively up from the requiring module's location
+ moduleDirectories: [
+ "node_modules"
+ ],
+
+ // An array of file extensions your modules use
+ // moduleFileExtensions: [
+ // "js",
+ // "jsx",
+ // "ts",
+ // "tsx",
+ // "json",
+ // "node"
+ // ],
+
+ // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
+ // moduleNameMapper: {},
+
+ // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
+ // modulePathIgnorePatterns: [],
+
+ // Activates notifications for test results
+ // notify: false,
+
+ // An enum that specifies notification mode. Requires { notify: true }
+ // notifyMode: "failure-change",
+
+ // A preset that is used as a base for Jest's configuration
+ // preset: undefined,
+
+ // Run tests from one or more projects
+ // projects: undefined,
+
+ // Use this configuration option to add custom reporters to Jest
+ // reporters: undefined,
+
+ // Automatically reset mock state between every test
+ // resetMocks: false,
+
+ // Reset the module registry before running each individual test
+ // resetModules: false,
+
+ // A path to a custom resolver
+ // resolver: undefined,
+
+ // Automatically restore mock state between every test
+ restoreMocks: true,
+
+ // The root directory that Jest should scan for tests and modules within
+ // rootDir: undefined,
+
+ // A list of paths to directories that Jest should use to search for files in
+ // roots: [
+ // ""
+ // ],
+
+ // Allows you to use a custom runner instead of Jest's default test runner
+ // runner: "jest-runner",
+
+ // The paths to modules that run some code to configure or set up the testing environment before each test
+ // setupFiles: [],
+
+ // A list of paths to modules that run some code to configure or set up the testing framework before each test
+ // setupFilesAfterEnv: [],
+
+ // The number of seconds after which a test is considered as slow and reported as such in the results.
+ // slowTestThreshold: 5,
+
+ // A list of paths to snapshot serializer modules Jest should use for snapshot testing
+ // snapshotSerializers: [],
+
+ // The test environment that will be used for testing
+ // testEnvironment: "jest-environment-node",
+
+ // Options that will be passed to the testEnvironment
+ // testEnvironmentOptions: {},
+
+ // Adds a location field to test results
+ // testLocationInResults: false,
+
+ // The glob patterns Jest uses to detect test files
+ // testMatch: [
+ // "**/__tests__/**/*.[jt]s?(x)",
+ // "**/?(*.)+(spec|test).[tj]s?(x)"
+ // ],
+
+ // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
+ testPathIgnorePatterns: [
+ "\\\\node_modules\\\\"
+ ],
+
+ // The regexp pattern or array of patterns that Jest uses to detect test files
+ // testRegex: [],
+
+ // This option allows the use of a custom results processor
+ testResultsProcessor: "jest-sonar-reporter",
+
+ // This option allows use of a custom test runner
+ // testRunner: "jest-circus/runner",
+
+ // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
+ testEnvironmentOptions: {
+ url: "http://localhost:3000"
+ },
+
+ // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
+ // timers: "real",
+
+ // A map from regular expressions to paths to transformers
+ // transform: undefined,
+
+ // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
+ transformIgnorePatterns: [
+ "\\\\node_modules\\\\",
+ "\\.pnp\\.[^\\\\]+$"
+ ],
+
+ // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
+ // unmockedModulePathPatterns: undefined,
+
+ // Indicates whether each individual test should be reported during the run
+ // verbose: undefined,
+
+ // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
+ // watchPathIgnorePatterns: [],
+
+ // Whether to use watchman for file crawling
+ // watchman: true,
+};
diff --git a/worklenz-backend/new b/worklenz-backend/new
new file mode 100644
index 00000000..bb69f466
--- /dev/null
+++ b/worklenz-backend/new
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+const path = require("path");
+const slug = require("slugify");
+const { exec } = require("child_process");
+const [, , cmd, name] = process.argv;
+
+function s(value) {
+ return slug(value, {
+ replacement: "-", // replace spaces with replacement
+ remove: /[\/()._]/g, // regex to remove characters
+ lower: true, // result in lower case
+ });
+}
+
+function log(value) {
+ console.log(value);
+}
+
+function generate_controller() {
+ const pro = exec(`node cli/generate-controller ${s(name).replace(/controller/, "")}`);
+ pro.stdout.on("data", (data) => {
+ log(data.toString());
+ });
+
+ pro.stderr.on("data", (data) => {
+ log(`${data.toString()}`);
+ });
+
+ pro.on("exit", (code) => {
+ log(`Process exit with code ${code}.`);
+ });
+}
+
+function create_release() {
+ const release = exec(`node cli/mkrelease`);
+
+ release.stdout.on("data", (data) => {
+ log(data.toString());
+ });
+
+ release.stderr.on("data", (data) => {
+ log(`${data.toString()}`);
+ });
+
+ release.on("exit", (code) => {
+ log(`Build success with code ${code}.`);
+ log("Start Coping");
+ });
+}
+
+function generate_validator() {
+ const pro = exec(`node cli/generate-validator ${s(name).replace(/validator|validators/, "")}`);
+ pro.stdout.on("data", (data) => {
+ log(data.toString());
+ });
+
+ pro.stderr.on("data", (data) => {
+ log(`${data.toString()}`);
+ });
+
+ pro.on("exit", (code) => {
+ log(`Process exit with code ${code}.`);
+ });
+}
+
+switch (cmd) {
+ case "controller":
+ generate_controller();
+ break;
+ case "release":
+ create_release();
+ break;
+ case "validator":
+ generate_validator();
+ break;
+ default:
+ break;
+}
diff --git a/worklenz-backend/package-lock.json b/worklenz-backend/package-lock.json
new file mode 100644
index 00000000..532cbfec
--- /dev/null
+++ b/worklenz-backend/package-lock.json
@@ -0,0 +1,16333 @@
+{
+ "name": "worklenz-backend",
+ "version": "1.4.16",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "worklenz-backend",
+ "version": "1.4.16",
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.378.0",
+ "@aws-sdk/client-ses": "^3.378.0",
+ "@aws-sdk/s3-request-presigner": "^3.378.0",
+ "@aws-sdk/util-format-url": "^3.357.0",
+ "axios": "^1.6.0",
+ "bcrypt": "^5.1.0",
+ "bluebird": "^3.7.2",
+ "compression": "^1.7.4",
+ "connect-flash": "^0.1.1",
+ "connect-pg-simple": "^7.0.0",
+ "cookie-parser": "~1.4.4",
+ "cors": "^2.8.5",
+ "cron": "^2.4.0",
+ "csurf": "^1.11.0",
+ "debug": "^4.3.4",
+ "dotenv": "^16.3.1",
+ "exceljs": "^4.3.0",
+ "express": "^4.18.2",
+ "express-rate-limit": "^6.8.0",
+ "express-session": "^1.17.3",
+ "express-validator": "^6.15.0",
+ "helmet": "^6.2.0",
+ "hpp": "^0.2.3",
+ "http-errors": "^2.0.0",
+ "jsonschema": "^1.4.1",
+ "jsonwebtoken": "^9.0.1",
+ "lodash": "^4.17.21",
+ "mime-types": "^2.1.35",
+ "moment": "^2.29.4",
+ "moment-timezone": "^0.5.43",
+ "morgan": "^1.10.0",
+ "nanoid": "^3.3.6",
+ "passport": "^0.5.3",
+ "passport-google-oauth2": "^0.2.0",
+ "passport-google-oauth20": "^2.0.0",
+ "passport-local": "^1.0.0",
+ "path": "^0.12.7",
+ "pg": "^8.11.1",
+ "pg-native": "^3.0.1",
+ "pug": "^3.0.2",
+ "redis": "^4.6.7",
+ "sanitize-html": "^2.11.0",
+ "segfault-handler": "^1.3.0",
+ "sharp": "^0.32.6",
+ "slugify": "^1.6.6",
+ "socket.io": "^4.7.1",
+ "uglify-js": "^3.17.4",
+ "winston": "^3.10.0",
+ "xss-filters": "^1.2.7"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.22.9",
+ "@babel/preset-typescript": "^7.22.5",
+ "@types/bcrypt": "^5.0.0",
+ "@types/bluebird": "^3.5.38",
+ "@types/compression": "^1.7.2",
+ "@types/connect-flash": "^0.0.37",
+ "@types/cookie-parser": "^1.4.3",
+ "@types/cron": "^2.0.1",
+ "@types/csurf": "^1.11.2",
+ "@types/express": "^4.17.17",
+ "@types/express-brute": "^1.0.2",
+ "@types/express-brute-redis": "^0.0.4",
+ "@types/express-rate-limit": "^6.0.0",
+ "@types/express-session": "^1.17.7",
+ "@types/express-validator": "^3.0.0",
+ "@types/fs-extra": "^9.0.13",
+ "@types/hpp": "^0.2.2",
+ "@types/http-errors": "^1.8.2",
+ "@types/jest": "^28.1.8",
+ "@types/jsonwebtoken": "^9.0.2",
+ "@types/lodash": "^4.14.196",
+ "@types/mime-types": "^2.1.1",
+ "@types/morgan": "^1.9.4",
+ "@types/node": "^18.17.1",
+ "@types/passport": "^1.0.12",
+ "@types/passport-google-oauth20": "^2.0.11",
+ "@types/passport-local": "^1.0.35",
+ "@types/pg": "^8.10.2",
+ "@types/pug": "^2.0.6",
+ "@types/redis": "^4.0.11",
+ "@types/sanitize-html": "^2.9.0",
+ "@types/sharp": "^0.31.1",
+ "@types/socket.io": "^3.0.2",
+ "@types/swagger-jsdoc": "^6.0.1",
+ "@types/toobusy-js": "^0.5.2",
+ "@types/uglify-js": "^3.17.1",
+ "@types/xss-filters": "^0.0.27",
+ "@typescript-eslint/eslint-plugin": "^5.62.0",
+ "@typescript-eslint/parser": "^5.62.0",
+ "chokidar": "^3.5.3",
+ "esbuild": "^0.17.19",
+ "esbuild-envfile-plugin": "^1.0.5",
+ "esbuild-node-externals": "^1.8.0",
+ "eslint": "^8.45.0",
+ "eslint-plugin-security": "^1.7.1",
+ "fs-extra": "^10.1.0",
+ "grunt": "^1.6.1",
+ "grunt-cli": "^1.4.3",
+ "grunt-contrib-clean": "^2.0.1",
+ "grunt-contrib-compress": "^2.0.0",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-contrib-uglify": "^5.2.2",
+ "grunt-contrib-watch": "^1.1.0",
+ "grunt-shell": "^4.0.0",
+ "grunt-sync": "^0.8.2",
+ "jest": "^28.1.3",
+ "jest-sonar-reporter": "^2.0.0",
+ "ncp": "^2.0.0",
+ "nodeman": "^1.1.2",
+ "swagger-jsdoc": "^6.2.8",
+ "ts-jest": "^28.0.8",
+ "ts-node": "^10.9.1",
+ "tslint": "^6.1.3",
+ "typescript": "^4.9.5"
+ },
+ "engines": {
+ "node": ">=16.13.0",
+ "npm": ">=8.11.0",
+ "yarn": "WARNING: Please use npm package manager instead of yarn"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
+ "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
+ "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
+ "dev": true,
+ "dependencies": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.6",
+ "call-me-maybe": "^1.0.1",
+ "js-yaml": "^4.1.0"
+ }
+ },
+ "node_modules/@apidevtools/openapi-schemas": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
+ "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@apidevtools/swagger-methods": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
+ "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
+ "dev": true
+ },
+ "node_modules/@apidevtools/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
+ "dev": true,
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^9.0.6",
+ "@apidevtools/openapi-schemas": "^2.0.4",
+ "@apidevtools/swagger-methods": "^3.0.2",
+ "@jsdevtools/ono": "^7.1.3",
+ "call-me-maybe": "^1.0.1",
+ "z-schema": "^5.0.1"
+ },
+ "peerDependencies": {
+ "openapi-types": ">=7"
+ }
+ },
+ "node_modules/@aws-crypto/crc32": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
+ "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==",
+ "dependencies": {
+ "@aws-crypto/util": "^3.0.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/crc32/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz",
+ "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==",
+ "dependencies": {
+ "@aws-crypto/util": "^3.0.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/crc32c/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/ie11-detection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz",
+ "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==",
+ "dependencies": {
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/ie11-detection/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz",
+ "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==",
+ "dependencies": {
+ "@aws-crypto/ie11-detection": "^3.0.0",
+ "@aws-crypto/supports-web-crypto": "^3.0.0",
+ "@aws-crypto/util": "^3.0.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@aws-sdk/util-utf8-browser": "^3.0.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/sha256-browser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz",
+ "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==",
+ "dependencies": {
+ "@aws-crypto/ie11-detection": "^3.0.0",
+ "@aws-crypto/sha256-js": "^3.0.0",
+ "@aws-crypto/supports-web-crypto": "^3.0.0",
+ "@aws-crypto/util": "^3.0.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@aws-sdk/util-utf8-browser": "^3.0.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz",
+ "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==",
+ "dependencies": {
+ "@aws-crypto/util": "^3.0.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz",
+ "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==",
+ "dependencies": {
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz",
+ "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-utf8-browser": "^3.0.0",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.378.0.tgz",
+ "integrity": "sha512-FW1CFT6Kt2Y+IiFPCd70VapcZBkS1ZhpPZttpJeugE8T2Hye1fwQDDvAwd3Slo4zMkTL+cWQkfFJSNB1Dez/pQ==",
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "3.0.0",
+ "@aws-crypto/sha256-browser": "3.0.0",
+ "@aws-crypto/sha256-js": "3.0.0",
+ "@aws-sdk/client-sts": "3.378.0",
+ "@aws-sdk/credential-provider-node": "3.378.0",
+ "@aws-sdk/middleware-bucket-endpoint": "3.378.0",
+ "@aws-sdk/middleware-expect-continue": "3.378.0",
+ "@aws-sdk/middleware-flexible-checksums": "3.378.0",
+ "@aws-sdk/middleware-host-header": "3.378.0",
+ "@aws-sdk/middleware-location-constraint": "3.378.0",
+ "@aws-sdk/middleware-logger": "3.378.0",
+ "@aws-sdk/middleware-recursion-detection": "3.378.0",
+ "@aws-sdk/middleware-sdk-s3": "3.378.0",
+ "@aws-sdk/middleware-signing": "3.378.0",
+ "@aws-sdk/middleware-ssec": "3.378.0",
+ "@aws-sdk/middleware-user-agent": "3.378.0",
+ "@aws-sdk/signature-v4-multi-region": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@aws-sdk/util-user-agent-browser": "3.378.0",
+ "@aws-sdk/util-user-agent-node": "3.378.0",
+ "@aws-sdk/xml-builder": "3.310.0",
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/eventstream-serde-browser": "^2.0.1",
+ "@smithy/eventstream-serde-config-resolver": "^2.0.1",
+ "@smithy/eventstream-serde-node": "^2.0.1",
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/hash-blob-browser": "^2.0.1",
+ "@smithy/hash-node": "^2.0.1",
+ "@smithy/hash-stream-node": "^2.0.1",
+ "@smithy/invalid-dependency": "^2.0.1",
+ "@smithy/md5-js": "^2.0.1",
+ "@smithy/middleware-content-length": "^2.0.1",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/middleware-retry": "^2.0.1",
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-body-length-browser": "^2.0.0",
+ "@smithy/util-body-length-node": "^2.0.0",
+ "@smithy/util-defaults-mode-browser": "^2.0.1",
+ "@smithy/util-defaults-mode-node": "^2.0.1",
+ "@smithy/util-retry": "^2.0.0",
+ "@smithy/util-stream": "^2.0.1",
+ "@smithy/util-utf8": "^2.0.0",
+ "@smithy/util-waiter": "^2.0.1",
+ "fast-xml-parser": "4.2.5",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-ses": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.378.0.tgz",
+ "integrity": "sha512-9mFGS1AYOjPcIfGeOdP8YGQSLSv+pldxTUcGytST9L8Vdj7yPY2bv/gLi+UUckTU1mKP98cQFcrBpzxawmTnDA==",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "3.0.0",
+ "@aws-crypto/sha256-js": "3.0.0",
+ "@aws-sdk/client-sts": "3.378.0",
+ "@aws-sdk/credential-provider-node": "3.378.0",
+ "@aws-sdk/middleware-host-header": "3.378.0",
+ "@aws-sdk/middleware-logger": "3.378.0",
+ "@aws-sdk/middleware-recursion-detection": "3.378.0",
+ "@aws-sdk/middleware-signing": "3.378.0",
+ "@aws-sdk/middleware-user-agent": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@aws-sdk/util-user-agent-browser": "3.378.0",
+ "@aws-sdk/util-user-agent-node": "3.378.0",
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/hash-node": "^2.0.1",
+ "@smithy/invalid-dependency": "^2.0.1",
+ "@smithy/middleware-content-length": "^2.0.1",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/middleware-retry": "^2.0.1",
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-body-length-browser": "^2.0.0",
+ "@smithy/util-body-length-node": "^2.0.0",
+ "@smithy/util-defaults-mode-browser": "^2.0.1",
+ "@smithy/util-defaults-mode-node": "^2.0.1",
+ "@smithy/util-retry": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "@smithy/util-waiter": "^2.0.1",
+ "fast-xml-parser": "4.2.5",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sso": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.378.0.tgz",
+ "integrity": "sha512-xQ2myljd4T0W46WQVHnT61PLiIoGqcIJA6euClvSQndKqXt8fnJP6/kn2r+APIsjey823pjkEP4mZq8gYDiOOw==",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "3.0.0",
+ "@aws-crypto/sha256-js": "3.0.0",
+ "@aws-sdk/middleware-host-header": "3.378.0",
+ "@aws-sdk/middleware-logger": "3.378.0",
+ "@aws-sdk/middleware-recursion-detection": "3.378.0",
+ "@aws-sdk/middleware-user-agent": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@aws-sdk/util-user-agent-browser": "3.378.0",
+ "@aws-sdk/util-user-agent-node": "3.378.0",
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/hash-node": "^2.0.1",
+ "@smithy/invalid-dependency": "^2.0.1",
+ "@smithy/middleware-content-length": "^2.0.1",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/middleware-retry": "^2.0.1",
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-body-length-browser": "^2.0.0",
+ "@smithy/util-body-length-node": "^2.0.0",
+ "@smithy/util-defaults-mode-browser": "^2.0.1",
+ "@smithy/util-defaults-mode-node": "^2.0.1",
+ "@smithy/util-retry": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sso-oidc": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.378.0.tgz",
+ "integrity": "sha512-+IcXH/W/TVzE0lMHuACgARgM/WxVbujGJzYUmDwj4E3uXjhTrRz69aeDk5z2EUggxKON9NOzHGZpm06VoS8uPA==",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "3.0.0",
+ "@aws-crypto/sha256-js": "3.0.0",
+ "@aws-sdk/middleware-host-header": "3.378.0",
+ "@aws-sdk/middleware-logger": "3.378.0",
+ "@aws-sdk/middleware-recursion-detection": "3.378.0",
+ "@aws-sdk/middleware-user-agent": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@aws-sdk/util-user-agent-browser": "3.378.0",
+ "@aws-sdk/util-user-agent-node": "3.378.0",
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/hash-node": "^2.0.1",
+ "@smithy/invalid-dependency": "^2.0.1",
+ "@smithy/middleware-content-length": "^2.0.1",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/middleware-retry": "^2.0.1",
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-body-length-browser": "^2.0.0",
+ "@smithy/util-body-length-node": "^2.0.0",
+ "@smithy/util-defaults-mode-browser": "^2.0.1",
+ "@smithy/util-defaults-mode-node": "^2.0.1",
+ "@smithy/util-retry": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sts": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.378.0.tgz",
+ "integrity": "sha512-u7y1I5BVjKEDK6ybA4c5smkbuoSFTBQqYX9qbCFYRErIA3qCICZB3duApcVRpdypKBzwYxUkLT/qKj4s9QTvrQ==",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "3.0.0",
+ "@aws-crypto/sha256-js": "3.0.0",
+ "@aws-sdk/credential-provider-node": "3.378.0",
+ "@aws-sdk/middleware-host-header": "3.378.0",
+ "@aws-sdk/middleware-logger": "3.378.0",
+ "@aws-sdk/middleware-recursion-detection": "3.378.0",
+ "@aws-sdk/middleware-sdk-sts": "3.378.0",
+ "@aws-sdk/middleware-signing": "3.378.0",
+ "@aws-sdk/middleware-user-agent": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@aws-sdk/util-user-agent-browser": "3.378.0",
+ "@aws-sdk/util-user-agent-node": "3.378.0",
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/hash-node": "^2.0.1",
+ "@smithy/invalid-dependency": "^2.0.1",
+ "@smithy/middleware-content-length": "^2.0.1",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/middleware-retry": "^2.0.1",
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-body-length-browser": "^2.0.0",
+ "@smithy/util-body-length-node": "^2.0.0",
+ "@smithy/util-defaults-mode-browser": "^2.0.1",
+ "@smithy/util-defaults-mode-node": "^2.0.1",
+ "@smithy/util-retry": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "fast-xml-parser": "4.2.5",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.378.0.tgz",
+ "integrity": "sha512-B2OVdO9kBClDwGgWTBLAQwFV8qYTYGyVujg++1FZFSFMt8ORFdZ5fNpErvJtiSjYiOOQMzyBeSNhKyYNXCiJjQ==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.378.0.tgz",
+ "integrity": "sha512-R34ELLCBTb+QkmWCaukNkT4vGeAipcL2wFN7Q2/WVSnJnRPPZSxzDK5rr78TiOPhRBu1k+aLDRNfslTZDknIIQ==",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.378.0",
+ "@aws-sdk/credential-provider-process": "3.378.0",
+ "@aws-sdk/credential-provider-sso": "3.378.0",
+ "@aws-sdk/credential-provider-web-identity": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/credential-provider-imds": "^2.0.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/shared-ini-file-loader": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.378.0.tgz",
+ "integrity": "sha512-vULsOsmcqSD+Prp/yl/o1gvQAKd2oHuqI8snh4G0RAkEvoyb7vx2l0ShCoXOVY/wM9PQH8nxBHmVbiAQfSndNg==",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.378.0",
+ "@aws-sdk/credential-provider-ini": "3.378.0",
+ "@aws-sdk/credential-provider-process": "3.378.0",
+ "@aws-sdk/credential-provider-sso": "3.378.0",
+ "@aws-sdk/credential-provider-web-identity": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/credential-provider-imds": "^2.0.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/shared-ini-file-loader": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.378.0.tgz",
+ "integrity": "sha512-KFTIy7u+wXj3eDua4rgS0tODzMnXtXhAm1RxzCW9FL5JLBBrd82ymCj1Dp72217Sw5Do6NjCnDTTNkCHZMA77w==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/shared-ini-file-loader": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.378.0.tgz",
+ "integrity": "sha512-lDPo/audYE/oERAef/VnHMe8THPCauH3Yu3DQYzCs+EWr1sIzp8vklWdMVQQI8cUlcLyYf4Dv9t8c+eJFZvrgw==",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.378.0",
+ "@aws-sdk/token-providers": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/shared-ini-file-loader": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.378.0.tgz",
+ "integrity": "sha512-GWjydOszhc4xDF8xuPtBvboglXQr0gwCW1oHAvmLcOT38+Hd6qnKywnMSeoXYRPgoKfF9TkWQgW1jxplzCG0UA==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-bucket-endpoint": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.378.0.tgz",
+ "integrity": "sha512-3o+AYU6JWUsPM49bWglCUOgNvySiHkbIma0J6F9a68e30vEDD0FUQtKzyHPZkF7iYDyesEl166gYjwVNAmASzw==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-arn-parser": "3.310.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-config-provider": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-expect-continue": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.378.0.tgz",
+ "integrity": "sha512-8maaNQvza3/IGDbIyVQkUbGlo+Oc6SY1gVG50UMcTUX8nwZrD1/ko+ft+pd2EDb2n+0JritoDj4bjr6pdesNBg==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.378.0.tgz",
+ "integrity": "sha512-pHkcVTu2T+x/1fpPHMpRDpXY5zxDsjijv3C6Nz/nm3gQrZvQ3fYDrQdV3Oj6Xeg40B3kkcp/bzgDo7MDzG088A==",
+ "dependencies": {
+ "@aws-crypto/crc32": "3.0.0",
+ "@aws-crypto/crc32c": "3.0.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/is-array-buffer": "^2.0.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.378.0.tgz",
+ "integrity": "sha512-zzZZ8U3MxTgSW/bpr5wNbDuGUc/lPtB9c07bD/+F81KuGCOiPIl4PA4EyMI3tftPM9DbbcFX5ZwKi9vlZ4BWcw==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-location-constraint": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.378.0.tgz",
+ "integrity": "sha512-Nn43avmhsDnCKtD1gQ7Xl2pvuxypnN7vvLWFeHb+7CCDKx/sK+ta+1UchNNOxh8hKL+rfBYOD2+/ZvwRRkAnAA==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.378.0.tgz",
+ "integrity": "sha512-l1DyaDLm3KeBMNMuANI3scWh8Xvu248x+vw6Z7ExWOhGXFmQ1MW7YvASg/SdxWkhlF9HmkkTif1LdMB22x6QDA==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.378.0.tgz",
+ "integrity": "sha512-mUMfHAz0oGNIWiTZHTVJb+I515Hqs2zx1j36Le4MMiiaMkPW1SRUF1FIwGuc1wh6E8jB5q+XfEMriDjRi4TZRA==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.378.0.tgz",
+ "integrity": "sha512-6PeZmQTG/GURC/fpCy71znSgn9brPSzMTIW1/cBLqW9RUB2CXb0ZsbsMPwcsN3lFgd2UHeIcZjg7wBRum/Xk/Q==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-arn-parser": "3.310.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-sts": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.378.0.tgz",
+ "integrity": "sha512-uOoE4mvlJnR7NGIbCXQA3nI4qjWHfEETX4WzamjCQBTmoXBUlSU0hCRKvG5VHSpwI3XOu7dke9fFqbldseQzgw==",
+ "dependencies": {
+ "@aws-sdk/middleware-signing": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-signing": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.378.0.tgz",
+ "integrity": "sha512-XnEQUg1wkbakDMEcwpaPq4U1qn+jdGVyPLvcvcecw09yJj0+SIG5h3xWhBYVUxm9zEJUhIYc1DnNL2V5YFeCoQ==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/signature-v4": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-middleware": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-ssec": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.378.0.tgz",
+ "integrity": "sha512-WDT2LOd6OxlY1zkrRG9ZtW2vFms/dsqMg9VyE88RKG2oATxSXEhkr5zLbNVh3TyuUKnV9jydate56d/ECwHOHg==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.378.0.tgz",
+ "integrity": "sha512-gwMmJgfqFh0k/Tvb+agXcdbIp9pUmYRN868CfqpKiQ7UlN8DHNixuPYrdktLkUBoEvnxmZEKdt0EnkBCdBTIcw==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-endpoints": "3.378.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.378.0.tgz",
+ "integrity": "sha512-bvrfZ+pUstwyfBZuZxG/xozfxGarldjjVX9HMxj49o1vvbGOhwRKO93XRUzV5WDk4TqKi5YcErCur0ZoqSgm4w==",
+ "dependencies": {
+ "@aws-sdk/signature-v4-multi-region": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@aws-sdk/util-format-url": "3.378.0",
+ "@smithy/middleware-endpoint": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/smithy-client": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.378.0.tgz",
+ "integrity": "sha512-gtuABS7EeYZQeNzTrabY3Ruv4wWmoz4u8OMSGl47gYPDWA70WYEZ0aoi4zSGuKhXiqtVvTsO9wGEMIInwV5phQ==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/signature-v4": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/signature-v4-crt": "^3.118.0"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/signature-v4-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.378.0.tgz",
+ "integrity": "sha512-2J3XCwYcImKGSpv4YZ7wqt/H+P56/BAFAmZx/LqwZlkgg+arTGo76WbeM0CQCsgmKuS9xZEVlfH4z+d0H9aoyw==",
+ "dependencies": {
+ "@aws-sdk/client-sso-oidc": "3.378.0",
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/property-provider": "^2.0.0",
+ "@smithy/shared-ini-file-loader": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.378.0.tgz",
+ "integrity": "sha512-qP0CvR/ItgktmN8YXpGQglzzR/6s0nrsQ4zIfx3HMwpsBTwuouYahcCtF1Vr82P4NFcoDA412EJahJ2pIqEd+w==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-arn-parser": {
+ "version": "3.310.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.310.0.tgz",
+ "integrity": "sha512-jL8509owp/xB9+Or0pvn3Fe+b94qfklc2yPowZZIFAkFcCSIdkIglz18cPDWnYAcy9JGewpMS1COXKIUhZkJsA==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.378.0.tgz",
+ "integrity": "sha512-NU5C2l2xAXxpyB5nT0fIhahLPlJoJdzHWw4uC53KH9b4PrjHtgvgCN8beIsD3QxyfgeoM4A5J9Auo6WurfRnLw==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-format-url": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.378.0.tgz",
+ "integrity": "sha512-CtW2HnCq08ildVD7B5OPn1zOxBAMBjkDxqzOcLw3Rk9F6OKuMM9hawulU62tMtouJPC0QSS6eLoNOrYGch5ehQ==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/querystring-builder": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.310.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz",
+ "integrity": "sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.378.0.tgz",
+ "integrity": "sha512-FSCpagzftK1W+m7Ar6lpX7/Gr9y5P56nhFYz8U4EYQ4PkufS6czWX9YW+/FA5OYV0vlQ/SvPqMnzoHIPUNhZrQ==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/types": "^2.0.2",
+ "bowser": "^2.11.0",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.378.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.378.0.tgz",
+ "integrity": "sha512-IdwVJV0E96MkJeFte4dlWqvB+oiqCiZ5lOlheY3W9NynTuuX0GGYNC8Y9yIsV8Oava1+ujpJq0ww6qXdYxmO4A==",
+ "dependencies": {
+ "@aws-sdk/types": "3.378.0",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/util-utf8-browser": {
+ "version": "3.259.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz",
+ "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==",
+ "dependencies": {
+ "tslib": "^2.3.1"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.310.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.310.0.tgz",
+ "integrity": "sha512-TqELu4mOuSIKQCqj63fGVs86Yh+vBx5nHRpWKNUNhB2nPTpfbziTs5c1X358be3peVWA4wPxW7Nt53KIg1tnNw==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.22.13",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz",
+ "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.22.13",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz",
+ "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz",
+ "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==",
+ "dev": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.22.5",
+ "@babel/generator": "^7.22.9",
+ "@babel/helper-compilation-targets": "^7.22.9",
+ "@babel/helper-module-transforms": "^7.22.9",
+ "@babel/helpers": "^7.22.6",
+ "@babel/parser": "^7.22.7",
+ "@babel/template": "^7.22.5",
+ "@babel/traverse": "^7.22.8",
+ "@babel/types": "^7.22.5",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.2",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
+ "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.23.0",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
+ "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz",
+ "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz",
+ "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-validator-option": "^7.22.5",
+ "browserslist": "^4.21.9",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.9.tgz",
+ "integrity": "sha512-Pwyi89uO4YrGKxL/eNJ8lfEH55DnRloGPOseaA8NFNL6jAUnn+KccaISiFazCj5IolPPDjGSdzQzXVzODVRqUQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-member-expression-to-functions": "^7.22.5",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.9",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz",
+ "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "regexpu-core": "^5.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.2.tgz",
+ "integrity": "sha512-k0qnnOqHn5dK9pZpfD5XXZ9SojAITdCKRn2Lp6rnDGzIbaP0rHyMPk/4wsSxVBVz4RfN0q6VpXWP2pDGIoQ7hw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
+ "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
+ "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.15",
+ "@babel/types": "^7.23.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz",
+ "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz",
+ "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz",
+ "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz",
+ "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-simple-access": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/helper-validator-identifier": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
+ "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
+ "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz",
+ "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-wrap-function": "^7.22.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz",
+ "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-member-expression-to-functions": "^7.22.5",
+ "@babel/helper-optimise-call-expression": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz",
+ "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
+ "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz",
+ "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz",
+ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz",
+ "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.9.tgz",
+ "integrity": "sha512-sZ+QzfauuUEfxSEjKFmi3qDSHgLsTPK/pEpoD/qonZKOtTPTLbf59oabPQ4rKekt9lFcj/hTZaOhWwFYrgjk+Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/template": "^7.22.5",
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz",
+ "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.22.5",
+ "@babel/traverse": "^7.22.6",
+ "@babel/types": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz",
+ "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
+ "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz",
+ "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz",
+ "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-transform-optional-chaining": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-unicode-property-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz",
+ "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-static-block": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz",
+ "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-dynamic-import": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",
+ "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-export-namespace-from": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz",
+ "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz",
+ "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz",
+ "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
+ "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-private-property-in-object": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz",
+ "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz",
+ "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz",
+ "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.22.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz",
+ "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-remap-async-to-generator": "^7.22.5",
+ "@babel/plugin-syntax-async-generators": "^7.8.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz",
+ "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-remap-async-to-generator": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz",
+ "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz",
+ "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz",
+ "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz",
+ "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz",
+ "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-environment-visitor": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-optimise-call-expression": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz",
+ "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/template": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz",
+ "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz",
+ "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz",
+ "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz",
+ "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz",
+ "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz",
+ "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz",
+ "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz",
+ "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.5",
+ "@babel/helper-function-name": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz",
+ "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-json-strings": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz",
+ "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz",
+ "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz",
+ "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz",
+ "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz",
+ "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-simple-access": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz",
+ "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz",
+ "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz",
+ "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz",
+ "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz",
+ "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz",
+ "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz",
+ "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.5",
+ "@babel/helper-compilation-targets": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-transform-parameters": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz",
+ "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-replace-supers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz",
+ "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz",
+ "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz",
+ "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz",
+ "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz",
+ "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-create-class-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz",
+ "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz",
+ "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "regenerator-transform": "^0.15.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz",
+ "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz",
+ "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz",
+ "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz",
+ "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz",
+ "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz",
+ "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.9.tgz",
+ "integrity": "sha512-BnVR1CpKiuD0iobHPaM1iLvcwPYN2uVFAqoLVSpEDKWuOikoCv5HbKLxclhKYUXlWkX86DoZGtqI4XhbOsyrMg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.22.5",
+ "@babel/helper-create-class-features-plugin": "^7.22.9",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/plugin-syntax-typescript": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz",
+ "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz",
+ "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz",
+ "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz",
+ "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.22.5",
+ "@babel/helper-plugin-utils": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.22.9",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.9.tgz",
+ "integrity": "sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.9",
+ "@babel/helper-compilation-targets": "^7.22.9",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-option": "^7.22.5",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-class-properties": "^7.12.13",
+ "@babel/plugin-syntax-class-static-block": "^7.14.5",
+ "@babel/plugin-syntax-dynamic-import": "^7.8.3",
+ "@babel/plugin-syntax-export-namespace-from": "^7.8.3",
+ "@babel/plugin-syntax-import-assertions": "^7.22.5",
+ "@babel/plugin-syntax-import-attributes": "^7.22.5",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.10.4",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5",
+ "@babel/plugin-syntax-top-level-await": "^7.14.5",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.22.5",
+ "@babel/plugin-transform-async-generator-functions": "^7.22.7",
+ "@babel/plugin-transform-async-to-generator": "^7.22.5",
+ "@babel/plugin-transform-block-scoped-functions": "^7.22.5",
+ "@babel/plugin-transform-block-scoping": "^7.22.5",
+ "@babel/plugin-transform-class-properties": "^7.22.5",
+ "@babel/plugin-transform-class-static-block": "^7.22.5",
+ "@babel/plugin-transform-classes": "^7.22.6",
+ "@babel/plugin-transform-computed-properties": "^7.22.5",
+ "@babel/plugin-transform-destructuring": "^7.22.5",
+ "@babel/plugin-transform-dotall-regex": "^7.22.5",
+ "@babel/plugin-transform-duplicate-keys": "^7.22.5",
+ "@babel/plugin-transform-dynamic-import": "^7.22.5",
+ "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
+ "@babel/plugin-transform-export-namespace-from": "^7.22.5",
+ "@babel/plugin-transform-for-of": "^7.22.5",
+ "@babel/plugin-transform-function-name": "^7.22.5",
+ "@babel/plugin-transform-json-strings": "^7.22.5",
+ "@babel/plugin-transform-literals": "^7.22.5",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.22.5",
+ "@babel/plugin-transform-member-expression-literals": "^7.22.5",
+ "@babel/plugin-transform-modules-amd": "^7.22.5",
+ "@babel/plugin-transform-modules-commonjs": "^7.22.5",
+ "@babel/plugin-transform-modules-systemjs": "^7.22.5",
+ "@babel/plugin-transform-modules-umd": "^7.22.5",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5",
+ "@babel/plugin-transform-new-target": "^7.22.5",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5",
+ "@babel/plugin-transform-numeric-separator": "^7.22.5",
+ "@babel/plugin-transform-object-rest-spread": "^7.22.5",
+ "@babel/plugin-transform-object-super": "^7.22.5",
+ "@babel/plugin-transform-optional-catch-binding": "^7.22.5",
+ "@babel/plugin-transform-optional-chaining": "^7.22.6",
+ "@babel/plugin-transform-parameters": "^7.22.5",
+ "@babel/plugin-transform-private-methods": "^7.22.5",
+ "@babel/plugin-transform-private-property-in-object": "^7.22.5",
+ "@babel/plugin-transform-property-literals": "^7.22.5",
+ "@babel/plugin-transform-regenerator": "^7.22.5",
+ "@babel/plugin-transform-reserved-words": "^7.22.5",
+ "@babel/plugin-transform-shorthand-properties": "^7.22.5",
+ "@babel/plugin-transform-spread": "^7.22.5",
+ "@babel/plugin-transform-sticky-regex": "^7.22.5",
+ "@babel/plugin-transform-template-literals": "^7.22.5",
+ "@babel/plugin-transform-typeof-symbol": "^7.22.5",
+ "@babel/plugin-transform-unicode-escapes": "^7.22.5",
+ "@babel/plugin-transform-unicode-property-regex": "^7.22.5",
+ "@babel/plugin-transform-unicode-regex": "^7.22.5",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.22.5",
+ "@babel/preset-modules": "^0.1.5",
+ "@babel/types": "^7.22.5",
+ "babel-plugin-polyfill-corejs2": "^0.4.4",
+ "babel-plugin-polyfill-corejs3": "^0.8.2",
+ "babel-plugin-polyfill-regenerator": "^0.5.1",
+ "core-js-compat": "^3.31.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz",
+ "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/plugin-proposal-unicode-property-regex": "^7.4.4",
+ "@babel/plugin-transform-dotall-regex": "^7.4.4",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz",
+ "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "@babel/helper-validator-option": "^7.22.5",
+ "@babel/plugin-syntax-jsx": "^7.22.5",
+ "@babel/plugin-transform-modules-commonjs": "^7.22.5",
+ "@babel/plugin-transform-typescript": "^7.22.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==",
+ "dev": true
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.22.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz",
+ "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==",
+ "dev": true,
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.22.15",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
+ "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/parser": "^7.22.15",
+ "@babel/types": "^7.22.15"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.23.2",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+ "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.22.13",
+ "@babel/generator": "^7.23.0",
+ "@babel/helper-environment-visitor": "^7.22.20",
+ "@babel/helper-function-name": "^7.23.0",
+ "@babel/helper-hoist-variables": "^7.22.5",
+ "@babel/helper-split-export-declaration": "^7.22.6",
+ "@babel/parser": "^7.23.0",
+ "@babel/types": "^7.23.0",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.23.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
+ "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.22.5",
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true
+ },
+ "node_modules/@colors/colors": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@dabh/diagnostics": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
+ "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
+ "dependencies": {
+ "colorspace": "1.1.x",
+ "enabled": "2.0.x",
+ "kuler": "^2.0.0"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz",
+ "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.44.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz",
+ "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@fast-csv/format": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
+ "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==",
+ "dependencies": {
+ "@types/node": "^14.0.1",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isequal": "^4.5.0",
+ "lodash.isfunction": "^3.0.9",
+ "lodash.isnil": "^4.0.0"
+ }
+ },
+ "node_modules/@fast-csv/format/node_modules/@types/node": {
+ "version": "14.18.54",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz",
+ "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw=="
+ },
+ "node_modules/@fast-csv/parse": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz",
+ "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==",
+ "dependencies": {
+ "@types/node": "^14.0.1",
+ "lodash.escaperegexp": "^4.1.2",
+ "lodash.groupby": "^4.6.0",
+ "lodash.isfunction": "^3.0.9",
+ "lodash.isnil": "^4.0.0",
+ "lodash.isundefined": "^3.0.1",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
+ "node_modules/@fast-csv/parse/node_modules/@types/node": {
+ "version": "14.18.54",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz",
+ "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw=="
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz",
+ "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/console/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/console/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/console/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@jest/console/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@jest/console/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz",
+ "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^28.1.3",
+ "@jest/reporters": "^28.1.3",
+ "@jest/test-result": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^28.1.3",
+ "jest-config": "^28.1.3",
+ "jest-haste-map": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.3",
+ "jest-resolve-dependencies": "^28.1.3",
+ "jest-runner": "^28.1.3",
+ "jest-runtime": "^28.1.3",
+ "jest-snapshot": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jest-validate": "^28.1.3",
+ "jest-watcher": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "rimraf": "^3.0.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/core/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/core/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@jest/core/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@jest/core/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/core/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/environment": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz",
+ "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz",
+ "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==",
+ "dev": true,
+ "dependencies": {
+ "expect": "^28.1.3",
+ "jest-snapshot": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz",
+ "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==",
+ "dev": true,
+ "dependencies": {
+ "jest-get-type": "^28.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz",
+ "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@sinonjs/fake-timers": "^9.1.2",
+ "@types/node": "*",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz",
+ "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/expect": "^28.1.3",
+ "@jest/types": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz",
+ "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==",
+ "dev": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^28.1.3",
+ "@jest/test-result": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "terminal-link": "^2.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@jest/reporters/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz",
+ "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==",
+ "dev": true,
+ "dependencies": {
+ "@sinclair/typebox": "^0.24.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz",
+ "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz",
+ "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz",
+ "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/test-result": "^28.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz",
+ "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^28.1.3",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^1.4.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@jest/transform/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
+ "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/types/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@jest/types/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/@jest/types/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/@jest/types/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/@jest/types/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/types/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.18",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
+ "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
+ "dev": true
+ },
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
+ "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.7",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@redis/bloom": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
+ "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/client": {
+ "version": "1.5.8",
+ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.8.tgz",
+ "integrity": "sha512-xzElwHIO6rBAqzPeVnCzgvrnBEcFL1P0w8P65VNLRkdVW8rOE58f52hdj0BDgmsdOm4f1EoXPZtH4Fh7M/qUpw==",
+ "dependencies": {
+ "cluster-key-slot": "1.1.2",
+ "generic-pool": "3.9.0",
+ "yallist": "4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@redis/client/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/@redis/graph": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz",
+ "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/json": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
+ "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/search": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.3.tgz",
+ "integrity": "sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@redis/time-series": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz",
+ "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==",
+ "peerDependencies": {
+ "@redis/client": "^1.0.0"
+ }
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.24.51",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz",
+ "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==",
+ "dev": true
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "1.8.6",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
+ "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz",
+ "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "node_modules/@smithy/abort-controller": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.1.tgz",
+ "integrity": "sha512-0s7XjIbsTwZyUW9OwXQ8J6x1UiA1TNCh60Vaw56nHahL7kUZsLhmTlWiaxfLkFtO2Utkj8YewcpHTYpxaTzO+w==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-2.0.0.tgz",
+ "integrity": "sha512-k+J4GHJsMSAIQPChGBrjEmGS+WbPonCXesoqP9fynIqjn7rdOThdH8FAeCmokP9mxTYKQAKoHCLPzNlm6gh7Wg==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader-native": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-2.0.0.tgz",
+ "integrity": "sha512-HM8V2Rp1y8+1343tkZUKZllFhEQPNmpNdgFAncbTsxkZ18/gqjk23XXv3qGyXWp412f3o43ZZ1UZHVcHrpRnCQ==",
+ "dependencies": {
+ "@smithy/util-base64": "^2.0.0",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/config-resolver": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.1.tgz",
+ "integrity": "sha512-l83Pm7hV+8CBQOCmBRopWDtF+CURUJol7NsuPYvimiDhkC2F8Ba9T1imSFE+pD1UIJ9jlsDPAnZfPJT5cjnuEw==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-config-provider": "^2.0.0",
+ "@smithy/util-middleware": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.0.1.tgz",
+ "integrity": "sha512-8VxriuRINNEfVZjEFKBY75y9ZWAx73DZ5K/u+3LmB6r8WR2h3NaFxFKMlwlq0uzNdGhD1ouKBn9XWEGYHKiPLw==",
+ "dependencies": {
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/property-provider": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-codec": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.1.tgz",
+ "integrity": "sha512-/IiNB7gQM2y2ZC/GAWOWDa8+iXfhr1g9Xe5979cQEOdCWDISvrAiv18cn3OtIQUhbYOR3gm7QtCpkq1to2takQ==",
+ "dependencies": {
+ "@aws-crypto/crc32": "3.0.0",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-hex-encoding": "^2.0.0",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-browser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-2.0.1.tgz",
+ "integrity": "sha512-9E1/6ZGF7nB/Td3G1kcatU7VjjP8eZ/p/Q+0KsZc1AUPyv4lR15pmWnWj3iGBEGYI9qZBJ/7a/wPEPayabmA3Q==",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-config-resolver": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-2.0.1.tgz",
+ "integrity": "sha512-J8a+8HH8oDPIgq8Px/nPLfu9vpIjQ7XUPtP3orbs8KUh0GznNthSTy1xZP5RXjRqGQEkxPvsHf1po2+QOsgNFw==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-node": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-2.0.1.tgz",
+ "integrity": "sha512-wklowUz0zXJuqC7FMpriz66J8OAko3z6INTg+iMJWYB1bWv4pc5V7q36PxlZ0RKRbj0u+EThlozWgzE7Stz2Sw==",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-universal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-2.0.1.tgz",
+ "integrity": "sha512-WPPylIgVZ6wOYVgpF0Rs1LlocYyj248MRtKEEehnDvC+0tV7wmGt7H/SchCh10W4y4YUxuzPlW+mUvVMGmLSVg==",
+ "dependencies": {
+ "@smithy/eventstream-codec": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.0.1.tgz",
+ "integrity": "sha512-/SoU/ClazgcdOxgE4zA7RX8euiELwpsrKCSvulVQvu9zpmqJRyEJn8ZTWYFV17/eHOBdHTs9kqodhNhsNT+cUw==",
+ "dependencies": {
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/querystring-builder": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-base64": "^2.0.0",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/hash-blob-browser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-2.0.1.tgz",
+ "integrity": "sha512-i/o2+sHb4jDRz5nf2ilTTbC0nVmm4LO//FbODCAB7pbzMdywxbZ6z+q56FmEa8R+aFbtApxQ1SJ3umEiNz6IPg==",
+ "dependencies": {
+ "@smithy/chunked-blob-reader": "^2.0.0",
+ "@smithy/chunked-blob-reader-native": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/hash-node": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.1.tgz",
+ "integrity": "sha512-oTKYimQdF4psX54ZonpcIE+MXjMUWFxLCNosjPkJPFQ9whRX0K/PFX/+JZGRQh3zO9RlEOEUIbhy9NO+Wha6hw==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-buffer-from": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-stream-node": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-2.0.1.tgz",
+ "integrity": "sha512-AequnQdPRuXf4AuvvFlSjnkWI460xxhAd6y362gFtOE4jjJLLXblbMAXVFrkV8/pDMGNjpVegVSpRmHXZsbKhg==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/invalid-dependency": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.1.tgz",
+ "integrity": "sha512-2q/Eb0AE662zwyMV+z+TL7deBwcHCgaZZGc0RItamBE8kak3MzCi/EZCNoFWoBfxgQ4jfR12wm8KKsSXhJzJtQ==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz",
+ "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/md5-js": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-2.0.1.tgz",
+ "integrity": "sha512-8WWOtwWMmIDgTkRv1o3opy3ABsRXs4/XunETK53ckxQRAiOML1PlnqLBK9Uwk9bvOD6cpmsC6dioIfmKGpJ25w==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/middleware-content-length": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.1.tgz",
+ "integrity": "sha512-IZhRSk5GkVBcrKaqPXddBS2uKhaqwBgaSgbBb1OJyGsKe7SxRFbclWS0LqOR9fKUkDl+3lL8E2ffpo6EQg0igw==",
+ "dependencies": {
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-endpoint": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.0.1.tgz",
+ "integrity": "sha512-uz/KI1MBd9WHrrkVFZO4L4Wyv24raf0oR4EsOYEeG5jPJO5U+C7MZGLcMxX8gWERDn1sycBDqmGv8fjUMLxT6w==",
+ "dependencies": {
+ "@smithy/middleware-serde": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/url-parser": "^2.0.1",
+ "@smithy/util-middleware": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.1.tgz",
+ "integrity": "sha512-NKHF4i0gjSyjO6C0ZyjEpNqzGgIu7s8HOK6oT/1Jqws2Q1GynR1xV8XTUs1gKXeaNRzbzKQRewHHmfPwZjOtHA==",
+ "dependencies": {
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/service-error-classification": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-middleware": "^2.0.0",
+ "@smithy/util-retry": "^2.0.0",
+ "tslib": "^2.5.0",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-serde": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.1.tgz",
+ "integrity": "sha512-uKxPaC6ItH9ZXdpdqNtf8sda7GcU4SPMp0tomq/5lUg9oiMa/Q7+kD35MUrpKaX3IVXVrwEtkjCU9dogZ/RAUA==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-stack": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.0.tgz",
+ "integrity": "sha512-31XC1xNF65nlbc16yuh3wwTudmqs6qy4EseQUGF8A/p2m/5wdd/cnXJqpniy/XvXVwkHPz/GwV36HqzHtIKATQ==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/node-config-provider": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.0.1.tgz",
+ "integrity": "sha512-Zoel4CPkKRTQ2XxmozZUfqBYqjPKL53/SvTDhJHj+VBSiJy6MXRav1iDCyFPS92t40Uh+Yi+Km5Ch3hQ+c/zSA==",
+ "dependencies": {
+ "@smithy/property-provider": "^2.0.1",
+ "@smithy/shared-ini-file-loader": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.0.1.tgz",
+ "integrity": "sha512-Zv3fxk3p9tsmPT2CKMsbuwbbxnq2gzLDIulxv+yI6aE+02WPYorObbbe9gh7SW3weadMODL1vTfOoJ9yFypDzg==",
+ "dependencies": {
+ "@smithy/abort-controller": "^2.0.1",
+ "@smithy/protocol-http": "^2.0.1",
+ "@smithy/querystring-builder": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/property-provider": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.1.tgz",
+ "integrity": "sha512-pmJRyY9SF6sutWIktIhe+bUdSQDxv/qZ4mYr3/u+u45riTPN7nmRxPo+e4sjWVoM0caKFjRSlj3tf5teRFy0Vg==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/protocol-http": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-2.0.1.tgz",
+ "integrity": "sha512-mrkMAp0wtaDEIkgRObWYxI1Kun1tm6Iu6rK+X4utb6Ah7Uc3Kk4VIWwK/rBHdYGReiLIrxFCB1rq4a2gyZnSgg==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-builder": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.1.tgz",
+ "integrity": "sha512-bp+93WFzx1FojVEIeFPtG0A1pKsFdCUcZvVdZdRlmNooOUrz9Mm9bneRd8hDwAQ37pxiZkCOxopSXXRQN10mYw==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-uri-escape": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-parser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.1.tgz",
+ "integrity": "sha512-h+e7k1z+IvI2sSbUBG9Aq46JsgLl4UqIUl6aigAlRBj+P6ocNXpM6Yn1vMBw5ijtXeZbYpd1YvCxwDgdw3jhmg==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/service-error-classification": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.0.tgz",
+ "integrity": "sha512-2z5Nafy1O0cTf69wKyNjGW/sNVMiqDnb4jgwfMG8ye8KnFJ5qmJpDccwIbJNhXIfbsxTg9SEec2oe1cexhMJvw==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/shared-ini-file-loader": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.0.1.tgz",
+ "integrity": "sha512-a463YiZrPGvM+F336rIF8pLfQsHAdCRAn/BiI/EWzg5xLoxbC7GSxIgliDDXrOu0z8gT3nhVsif85eU6jyct3A==",
+ "dependencies": {
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.1.tgz",
+ "integrity": "sha512-jztv5Mirca42ilxmMDjzLdXcoAmRhZskGafGL49sRo5u7swEZcToEFrq6vtX5YMbSyTVrE9Teog5EFexY5Ff2Q==",
+ "dependencies": {
+ "@smithy/eventstream-codec": "^2.0.1",
+ "@smithy/is-array-buffer": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-hex-encoding": "^2.0.0",
+ "@smithy/util-middleware": "^2.0.0",
+ "@smithy/util-uri-escape": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/smithy-client": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.0.1.tgz",
+ "integrity": "sha512-LHC5m6tYpEu1iNbONfvMbwtErboyTZJfEIPoD78Ei5MVr36vZQCaCla5mvo36+q/a2NAk2//fA5Rx3I1Kf7+lQ==",
+ "dependencies": {
+ "@smithy/middleware-stack": "^2.0.0",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-stream": "^2.0.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.0.2.tgz",
+ "integrity": "sha512-wcymEjIXQ9+NEfE5Yt5TInAqe1o4n+Nh+rh00AwoazppmUt8tdo6URhc5gkDcOYrcvlDVAZE7uG69nDpEGUKxw==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/url-parser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.1.tgz",
+ "integrity": "sha512-NpHVOAwddo+OyyIoujDL9zGL96piHWrTNXqltWmBvlUoWgt1HPyBuKs6oHjioyFnNZXUqveTOkEEq0U5w6Uv8A==",
+ "dependencies": {
+ "@smithy/querystring-parser": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/util-base64": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.0.tgz",
+ "integrity": "sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-browser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz",
+ "integrity": "sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.0.0.tgz",
+ "integrity": "sha512-ZV7Z/WHTMxHJe/xL/56qZwSUcl63/5aaPAGjkfynJm4poILjdD4GmFI+V+YWabh2WJIjwTKZ5PNsuvPQKt93Mg==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz",
+ "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-config-provider": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz",
+ "integrity": "sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.1.tgz",
+ "integrity": "sha512-w72Qwsb+IaEYEFtYICn0Do42eFju78hTaBzzJfT107lFOPdbjWjKnFutV+6GL/nZd5HWXY7ccAKka++C3NrjHw==",
+ "dependencies": {
+ "@smithy/property-provider": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "bowser": "^2.11.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-node": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.1.tgz",
+ "integrity": "sha512-dNF45caelEBambo0SgkzQ0v76m4YM+aFKZNTtSafy7P5dVF8TbjZuR2UX1A5gJABD9XK6lzN+v/9Yfzj/EDgGg==",
+ "dependencies": {
+ "@smithy/config-resolver": "^2.0.1",
+ "@smithy/credential-provider-imds": "^2.0.1",
+ "@smithy/node-config-provider": "^2.0.1",
+ "@smithy/property-provider": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/@smithy/util-hex-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz",
+ "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-middleware": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.0.tgz",
+ "integrity": "sha512-eCWX4ECuDHn1wuyyDdGdUWnT4OGyIzV0LN1xRttBFMPI9Ff/4heSHVxneyiMtOB//zpXWCha1/SWHJOZstG7kA==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-retry": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.0.tgz",
+ "integrity": "sha512-/dvJ8afrElasuiiIttRJeoS2sy8YXpksQwiM/TcepqdRVp7u4ejd9C4IQURHNjlfPUT7Y6lCDSa2zQJbdHhVTg==",
+ "dependencies": {
+ "@smithy/service-error-classification": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.1.tgz",
+ "integrity": "sha512-2a0IOtwIKC46EEo7E7cxDN8u2jwOiYYJqcFKA6rd5rdXqKakHT2Gc+AqHWngr0IEHUfW92zX12wRQKwyoqZf2Q==",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^2.0.1",
+ "@smithy/node-http-handler": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "@smithy/util-base64": "^2.0.0",
+ "@smithy/util-buffer-from": "^2.0.0",
+ "@smithy/util-hex-encoding": "^2.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-uri-escape": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz",
+ "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==",
+ "dependencies": {
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.0.tgz",
+ "integrity": "sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.0.0",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@smithy/util-waiter": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-2.0.1.tgz",
+ "integrity": "sha512-bSyGFicPRYuGFFWAr72UvYI7tE7KmEeFJJ5iaLuTTdo8RGaNBZ2kE25coGtzrejYh9AhwSfckBvbxgEDxIxhlA==",
+ "dependencies": {
+ "@smithy/abort-controller": "^2.0.1",
+ "@smithy/types": "^2.0.2",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
+ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
+ "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+ "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+ "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+ "dev": true
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+ "dev": true
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz",
+ "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.20.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz",
+ "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.20.7"
+ }
+ },
+ "node_modules/@types/bcrypt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
+ "integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bluebird": {
+ "version": "3.5.38",
+ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.38.tgz",
+ "integrity": "sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg==",
+ "dev": true
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
+ "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
+ "dev": true,
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/compression": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz",
+ "integrity": "sha512-lwEL4M/uAGWngWFLSG87ZDr2kLrbuR8p7X+QZB1OQlT+qkHsCPDVFnHPyXf4Vyl4yDDorNY+mAhosxkCvppatg==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.35",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
+ "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-flash": {
+ "version": "0.0.37",
+ "resolved": "https://registry.npmjs.org/@types/connect-flash/-/connect-flash-0.0.37.tgz",
+ "integrity": "sha512-SfmGGYpKvPfZeA+v74FS0HlYqVsx8Inb4d3px99kz2xSMx/IQiz/K/i+7MHTmk/OkE+0suZX108tHrQJ8QEGag==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+ },
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
+ "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.13",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
+ "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cron": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.1.tgz",
+ "integrity": "sha512-WHa/1rtNtD2Q/H0+YTTZoty+/5rcE66iAFX2IY+JuUoOACsevYyFkSYu/2vdw+G5LrmO7Lxowrqm0av4k3qWNQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/luxon": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/csurf": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz",
+ "integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/express-serve-static-core": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.17",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
+ "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-brute": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@types/express-brute/-/express-brute-1.0.2.tgz",
+ "integrity": "sha512-p+3ks+pW04poJobPxyEK3FLnBhEbEAVYhc6QXXBoVBzw5yfW+HobKvgCnaQ6d/egBym+tDXGKIuGoAAZbaJadw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/express-brute-redis": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/@types/express-brute-redis/-/express-brute-redis-0.0.4.tgz",
+ "integrity": "sha512-pjarPr7id4sPuTMeltb8Z50rxbJxjAhLtkDbaiobeIcjnN1i+vwhq4YOeNTyAJneUPP0lossi0uvuvx9Of/zJg==",
+ "dev": true,
+ "dependencies": {
+ "@types/redis": "^2.8.0"
+ }
+ },
+ "node_modules/@types/express-brute-redis/node_modules/@types/redis": {
+ "version": "2.8.32",
+ "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
+ "integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express-rate-limit": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@types/express-rate-limit/-/express-rate-limit-6.0.0.tgz",
+ "integrity": "sha512-nZxo3nwU20EkTl/f2eGdndQkDIJYwkXIX4S3Vrp2jMdSdFJ6AWtIda8gOz0wiMuOFoeH/UUlCAiacz3x3eWNFA==",
+ "deprecated": "This is a stub types definition. express-rate-limit provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "express-rate-limit": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.17.35",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz",
+ "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/express-session": {
+ "version": "1.17.7",
+ "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
+ "integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/express-validator": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/express-validator/-/express-validator-3.0.0.tgz",
+ "integrity": "sha512-LusnB0YhTXpBT25PXyGPQlK7leE1e41Vezq1hHEUwjfkopM1Pkv2X2Ppxqh9c+w/HZ6Udzki8AJotKNjDTGdkQ==",
+ "deprecated": "This is a stub types definition for express-validator (https://github.com/ctavan/express-validator). express-validator provides its own type definitions, so you don't need @types/express-validator installed!",
+ "dev": true,
+ "dependencies": {
+ "express-validator": "*"
+ }
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "9.0.13",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
+ "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz",
+ "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/hpp": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@types/hpp/-/hpp-0.2.2.tgz",
+ "integrity": "sha512-BLgsawqFFbS3tFUr+mcBRfst+DumnSfi4PgyNeJAGk0eIxm7lKX1axmHVlbgKNAZS0caZA5/LSopuj0T2LKRPw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz",
+ "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+ "dev": true
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
+ "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/jest": {
+ "version": "28.1.8",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz",
+ "integrity": "sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==",
+ "dev": true,
+ "dependencies": {
+ "expect": "^28.0.0",
+ "pretty-format": "^28.0.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "dev": true
+ },
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.14.196",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.196.tgz",
+ "integrity": "sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==",
+ "dev": true
+ },
+ "node_modules/@types/luxon": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.1.tgz",
+ "integrity": "sha512-XOS5nBcgEeP2PpcqJHjCWhUCAzGfXIU8ILOSLpx2FhxqMW9KdxgCGXNOEKGVBfveKtIpztHzKK5vSRVLyW/NqA==",
+ "dev": true
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
+ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
+ "dev": true
+ },
+ "node_modules/@types/mime-types": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
+ "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==",
+ "dev": true
+ },
+ "node_modules/@types/morgan": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.4.tgz",
+ "integrity": "sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.17.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.1.tgz",
+ "integrity": "sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw=="
+ },
+ "node_modules/@types/oauth": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz",
+ "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/passport": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz",
+ "integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/passport-google-oauth20": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.11.tgz",
+ "integrity": "sha512-9XMT1GfwhZL7UQEiCepLef55RNPHkbrCtsU7rsWPTEOsmu5qVIW8nSemtB4p+P24CuOhA+IKkv8LsPThYghGww==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*",
+ "@types/passport-oauth2": "*"
+ }
+ },
+ "node_modules/@types/passport-local": {
+ "version": "1.0.35",
+ "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz",
+ "integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*",
+ "@types/passport-strategy": "*"
+ }
+ },
+ "node_modules/@types/passport-oauth2": {
+ "version": "1.4.12",
+ "resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.12.tgz",
+ "integrity": "sha512-RZg6cYTyEGinrZn/7REYQds6zrTxoBorX1/fdaz5UHzkG8xdFE7QQxkJagCr2ETzGII58FAFDmnmbTUVMrltNA==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/oauth": "*",
+ "@types/passport": "*"
+ }
+ },
+ "node_modules/@types/passport-strategy": {
+ "version": "0.2.35",
+ "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
+ "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.10.2",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz",
+ "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^4.0.1"
+ }
+ },
+ "node_modules/@types/prettier": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
+ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
+ "dev": true
+ },
+ "node_modules/@types/pug": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
+ "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==",
+ "dev": true
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
+ "dev": true
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
+ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
+ "dev": true
+ },
+ "node_modules/@types/redis": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz",
+ "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==",
+ "deprecated": "This is a stub types definition. redis provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "redis": "*"
+ }
+ },
+ "node_modules/@types/sanitize-html": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz",
+ "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==",
+ "dev": true,
+ "dependencies": {
+ "htmlparser2": "^8.0.0"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "dev": true
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz",
+ "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/mime": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sharp": {
+ "version": "0.31.1",
+ "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz",
+ "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/socket.io": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-3.0.2.tgz",
+ "integrity": "sha512-pu0sN9m5VjCxBZVK8hW37ZcMe8rjn4HHggBN5CbaRTvFwv5jOmuIRZEuddsBPa9Th0ts0SIo3Niukq+95cMBbQ==",
+ "deprecated": "This is a stub types definition. socket.io provides its own type definitions, so you do not need this installed.",
+ "dev": true,
+ "dependencies": {
+ "socket.io": "*"
+ }
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
+ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
+ "dev": true
+ },
+ "node_modules/@types/swagger-jsdoc": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.1.tgz",
+ "integrity": "sha512-+MUpcbyxD528dECUBCEVm6abNuORdbuGjbrUdHDeAQ+rkPuo2a+L4N02WJHF3bonSSE6SJ3dUJwF2V6+cHnf0w==",
+ "dev": true
+ },
+ "node_modules/@types/toobusy-js": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@types/toobusy-js/-/toobusy-js-0.5.2.tgz",
+ "integrity": "sha512-jK/CMvC5h/ECMhWRNjeFYIZzGFFvjt38+zbMndFKyDUHl1dm89SX7u8njoNHQKrW+OZoqxteBLvtSBFQOdGyHA==",
+ "dev": true
+ },
+ "node_modules/@types/triple-beam": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz",
+ "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g=="
+ },
+ "node_modules/@types/uglify-js": {
+ "version": "3.17.1",
+ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz",
+ "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==",
+ "dev": true,
+ "dependencies": {
+ "source-map": "^0.6.1"
+ }
+ },
+ "node_modules/@types/xss-filters": {
+ "version": "0.0.27",
+ "resolved": "https://registry.npmjs.org/@types/xss-filters/-/xss-filters-0.0.27.tgz",
+ "integrity": "sha512-ctN3f7vl4tBXa+W11hm0oDwp67K6SYK07h4OmNgaEoIOVJ/rksnc2prpbjK+Ju3/fYIa3HQaH4x9Y525CXFOow==",
+ "dev": true
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.24",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz",
+ "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==",
+ "dev": true,
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
+ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
+ "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
+ "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
+ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="
+ },
+ "node_modules/archiver": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz",
+ "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==",
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "async": "^3.2.3",
+ "buffer-crc32": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "readdir-glob": "^1.0.0",
+ "tar-stream": "^2.2.0",
+ "zip-stream": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/archiver-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
+ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
+ "dependencies": {
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.0",
+ "lazystream": "^1.0.0",
+ "lodash.defaults": "^4.2.0",
+ "lodash.difference": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.union": "^4.6.0",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/archiver-utils/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
+ "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/arg": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+ "dev": true
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/array-each": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+ "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/array-slice": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
+ "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
+ },
+ "node_modules/assert-never": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.2.1.tgz",
+ "integrity": "sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw=="
+ },
+ "node_modules/async": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
+ "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/axios": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.4",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
+ "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw=="
+ },
+ "node_modules/babel-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
+ "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==",
+ "dev": true,
+ "dependencies": {
+ "@jest/transform": "^28.1.3",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^28.1.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/babel-jest/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/babel-jest/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/babel-jest/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/babel-jest/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-jest/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz",
+ "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.5.tgz",
+ "integrity": "sha512-19hwUH5FKl49JEsvyTcoHakh6BE0wgXLLptIyKZ3PijHc/Ci521wygORCUCCred+E/twuqRyAkE02BAWPmsHOg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-define-polyfill-provider": "^0.4.2",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.3.tgz",
+ "integrity": "sha512-z41XaniZL26WLrvjy7soabMXrfPWARN25PZoriDEiLMxAp50AUW3t35BGQUMg5xK3UrpVTtagIDklxYa+MhiNA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.4.2",
+ "core-js-compat": "^3.31.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.2.tgz",
+ "integrity": "sha512-tAlOptU0Xj34V1Y2PNTL4Y0FOJMDB6bZmoW39FeCQIhigGLkqu3Fj6uiXpxIf6Ij274ENdYx64y6Au+ZKlb1IA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.4.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
+ "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz",
+ "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==",
+ "dev": true,
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^28.1.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-walk": {
+ "version": "3.0.0-canary-5",
+ "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz",
+ "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==",
+ "dependencies": {
+ "@babel/types": "^7.9.6"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/base64id": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+ "engines": {
+ "node": "^4.5.0 || >= 5.9"
+ }
+ },
+ "node_modules/base64url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
+ "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/bcrypt": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
+ "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.10",
+ "node-addon-api": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.51",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
+ "dependencies": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
+ },
+ "node_modules/body": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz",
+ "integrity": "sha512-chUsBxGRtuElD6fmw1gHLpvnKdVLK302peeFa9ZqAEk8TyzZ3fygLyUEDDPTJvL9+Bor0dIwn6ePOsRM2y0zQQ==",
+ "dev": true,
+ "dependencies": {
+ "continuable-cache": "^0.3.1",
+ "error": "^7.0.0",
+ "raw-body": "~1.1.0",
+ "safe-json-parse": "~1.0.1"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
+ "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.11.0",
+ "raw-body": "2.5.1",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/body/node_modules/bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
+ "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==",
+ "dev": true
+ },
+ "node_modules/body/node_modules/raw-body": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz",
+ "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==",
+ "dev": true,
+ "dependencies": {
+ "bytes": "1",
+ "string_decoder": "0.10"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/body/node_modules/string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
+ "dev": true
+ },
+ "node_modules/bowser": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz",
+ "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
+ "node-releases": "^2.0.12",
+ "update-browserslist-db": "^1.0.11"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bs-logger": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+ "dev": true,
+ "dependencies": {
+ "fast-json-stable-stringify": "2.x"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/buffer-indexof-polyfill": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/buffer-writer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
+ "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/buffers": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+ "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
+ "engines": {
+ "node": ">=0.2.0"
+ }
+ },
+ "node_modules/builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-me-maybe": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
+ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
+ "dev": true
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001517",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz",
+ "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chainsaw": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+ "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
+ "dependencies": {
+ "traverse": ">=0.3.0 <0.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/character-parser": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz",
+ "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==",
+ "dependencies": {
+ "is-regex": "^1.0.3"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
+ "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
+ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==",
+ "dev": true
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
+ "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==",
+ "dev": true
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
+ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/color/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/colors": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+ "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/colorspace": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
+ "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
+ "dependencies": {
+ "color": "^3.1.3",
+ "text-hex": "1.0.x"
+ }
+ },
+ "node_modules/colorspace/node_modules/color": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
+ "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
+ "dependencies": {
+ "color-convert": "^1.9.3",
+ "color-string": "^1.6.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
+ "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/compress-commons": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
+ "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==",
+ "dependencies": {
+ "buffer-crc32": "^0.2.13",
+ "crc32-stream": "^4.0.2",
+ "normalize-path": "^3.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "dependencies": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/connect-flash": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz",
+ "integrity": "sha512-2rcfELQt/ZMP+SM/pG8PyhJRaLKp+6Hk2IUBNkEit09X+vwn3QsAL3ZbYtxUn7NVPzbMTSLRDhqe0B/eh30RYA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/connect-pg-simple": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/connect-pg-simple/-/connect-pg-simple-7.0.0.tgz",
+ "integrity": "sha512-fbNZUkxz8m+FRbctoxAU18DzRKp8GQSL+9gTJ0+LgSCElXLon2q8tDE8V74jUzf+w2ARZX8HKKyV0laX1NUZ/Q==",
+ "dependencies": {
+ "@types/pg": "^8.6.1",
+ "pg": "^8.7.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
+ },
+ "node_modules/constantinople": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz",
+ "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==",
+ "dependencies": {
+ "@babel/parser": "^7.6.0",
+ "@babel/types": "^7.6.1"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-disposition/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/continuable-cache": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz",
+ "integrity": "sha512-TF30kpKhTH8AGCG3dut0rdd/19B7Z+qCnrMoBLpyQu/2drZdNrrpcjPEoJeSVsQM+8KmWG5O56oPDjSSUsuTyA==",
+ "dev": true
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "dev": true
+ },
+ "node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "dependencies": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.31.1",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.1.tgz",
+ "integrity": "sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==",
+ "dev": true,
+ "dependencies": {
+ "browserslist": "^4.21.9"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/crc32-stream": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz",
+ "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==",
+ "dependencies": {
+ "crc-32": "^1.2.0",
+ "readable-stream": "^3.4.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "dev": true
+ },
+ "node_modules/cron": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/cron/-/cron-2.4.0.tgz",
+ "integrity": "sha512-Cx77ic1TyIAtUggr0oAhtS8MLzPBUqGNIvdDM7jE3oFIxfe8LXWI9q3iQN/H2CebAiMir53LQKWOhEKnzkJTAQ==",
+ "dependencies": {
+ "luxon": "^3.2.1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csrf": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
+ "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
+ "dependencies": {
+ "rndm": "1.2.0",
+ "tsscmp": "1.0.6",
+ "uid-safe": "2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csurf": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
+ "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
+ "deprecated": "Please use another csrf package",
+ "dependencies": {
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "csrf": "3.1.0",
+ "http-errors": "~1.7.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/csurf/node_modules/cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/csurf/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/csurf/node_modules/http-errors": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
+ "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/csurf/node_modules/setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "node_modules/csurf/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/csurf/node_modules/toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/dateformat": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+ "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.9",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
+ "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
+ "dev": true
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz",
+ "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/doctypes": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz",
+ "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ]
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
+ "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+ "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/motdotla/dotenv?sponsor=1"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "dev": true
+ },
+ "node_modules/duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "node_modules/duplexer2/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/duplexer2/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.473",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.473.tgz",
+ "integrity": "sha512-aVfC8+440vGfl06l8HKKn8/PD5jRfSnLkTTD65EFvU46igbpQRri1gxSzW9/+TeUlwYzrXk1sw867T96zlyECA==",
+ "dev": true
+ },
+ "node_modules/emittery": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
+ "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/enabled": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/engine.io": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz",
+ "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==",
+ "dependencies": {
+ "@types/cookie": "^0.4.1",
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "accepts": "~1.3.4",
+ "base64id": "2.0.0",
+ "cookie": "~0.4.1",
+ "cors": "~2.8.5",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.1.0",
+ "ws": "~8.11.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz",
+ "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz",
+ "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==",
+ "dev": true,
+ "dependencies": {
+ "string-template": "~0.2.1"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/esbuild-envfile-plugin": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/esbuild-envfile-plugin/-/esbuild-envfile-plugin-1.0.5.tgz",
+ "integrity": "sha512-AT6mUTI4pbVodwLRYOrXYeXmFCAKO3SpRqAktS0IlKvpohTgvw/cYWUgkOXpB46BhMwhjwucBKO5UVejNg/DPg==",
+ "dev": true,
+ "dependencies": {
+ "dotenv": "16.0.3"
+ }
+ },
+ "node_modules/esbuild-envfile-plugin/node_modules/dotenv": {
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
+ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild-node-externals": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.8.0.tgz",
+ "integrity": "sha512-pYslmT8Bl383UnfxzHQQRpCgBNIOwAzDaYheuIeI4CODxelsN/eQroVn5STDow5QOpRalMgWUR+R8LfSgUROcw==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^5.0.0",
+ "tslib": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "esbuild": "0.12 - 0.18"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.45.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz",
+ "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.4.0",
+ "@eslint/eslintrc": "^2.1.0",
+ "@eslint/js": "8.44.0",
+ "@humanwhocodes/config-array": "^0.11.10",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.0",
+ "eslint-visitor-keys": "^3.4.1",
+ "espree": "^9.6.0",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-security": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.7.1.tgz",
+ "integrity": "sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-regex": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+ "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.1.tgz",
+ "integrity": "sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter2": {
+ "version": "0.4.14",
+ "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+ "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==",
+ "dev": true
+ },
+ "node_modules/exceljs": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.3.0.tgz",
+ "integrity": "sha512-hTAeo5b5TPvf8Z02I2sKIT4kSfCnOO2bCxYX8ABqODCdAjppI3gI9VYiGCQQYVcBaBSKlFDMKlAQRqC+kV9O8w==",
+ "dependencies": {
+ "archiver": "^5.0.0",
+ "dayjs": "^1.8.34",
+ "fast-csv": "^4.3.1",
+ "jszip": "^3.5.0",
+ "readable-stream": "^3.6.0",
+ "saxes": "^5.0.1",
+ "tmp": "^0.2.0",
+ "unzipper": "^0.10.11",
+ "uuid": "^8.3.0"
+ },
+ "engines": {
+ "node": ">=8.3.0"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/execa/node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+ "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz",
+ "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==",
+ "dev": true,
+ "dependencies": {
+ "@jest/expect-utils": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.18.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
+ "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.1",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.5.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.2.0",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.11.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.18.0",
+ "serve-static": "1.15.0",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/express-rate-limit": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz",
+ "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==",
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "peerDependencies": {
+ "express": "^4 || ^5"
+ }
+ },
+ "node_modules/express-session": {
+ "version": "1.17.3",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
+ "integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
+ "dependencies": {
+ "cookie": "0.4.2",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-headers": "~1.0.2",
+ "parseurl": "~1.3.3",
+ "safe-buffer": "5.2.1",
+ "uid-safe": "~2.1.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/express-session/node_modules/cookie": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
+ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express-session/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express-session/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/express-session/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/express-validator": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.15.0.tgz",
+ "integrity": "sha512-r05VYoBL3i2pswuehoFSy+uM8NBuVaY7avp5qrYjQBDzagx2Z5A77FZqPT8/gNLF3HopWkIzaTFaC4JysWXLqg==",
+ "dependencies": {
+ "lodash": "^4.17.21",
+ "validator": "^13.9.0"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/express/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "dev": true
+ },
+ "node_modules/fast-csv": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz",
+ "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==",
+ "dependencies": {
+ "@fast-csv/format": "4.3.5",
+ "@fast-csv/parse": "4.3.6"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz",
+ "integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==",
+ "funding": [
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+ "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==",
+ "dev": true,
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
+ "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==",
+ "dev": true,
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/fecha": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
+ "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-sync-cmp": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz",
+ "integrity": "sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA==",
+ "dev": true
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+ "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz",
+ "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/fined": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "is-plain-object": "^2.0.3",
+ "object.defaults": "^1.1.0",
+ "object.pick": "^1.2.0",
+ "parse-filepath": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flagged-respawn": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
+ },
+ "node_modules/fn.name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.5",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
+ "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-in": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+ "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/for-own": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+ "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==",
+ "dev": true,
+ "dependencies": {
+ "for-in": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fs-minipass/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/fstream": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ },
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/fstream/node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/fstream/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/gauge": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
+ "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/gaze": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
+ "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==",
+ "dev": true,
+ "dependencies": {
+ "globule": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/generic-pool": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
+ "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/getobject": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz",
+ "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globule": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz",
+ "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==",
+ "dev": true,
+ "dependencies": {
+ "glob": "~7.1.1",
+ "lodash": "^4.17.21",
+ "minimatch": "~3.0.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/globule/node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globule/node_modules/minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/grunt": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz",
+ "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==",
+ "dev": true,
+ "dependencies": {
+ "dateformat": "~4.6.2",
+ "eventemitter2": "~0.4.13",
+ "exit": "~0.1.2",
+ "findup-sync": "~5.0.0",
+ "glob": "~7.1.6",
+ "grunt-cli": "~1.4.3",
+ "grunt-known-options": "~2.0.0",
+ "grunt-legacy-log": "~3.0.0",
+ "grunt-legacy-util": "~2.0.1",
+ "iconv-lite": "~0.6.3",
+ "js-yaml": "~3.14.0",
+ "minimatch": "~3.0.4",
+ "nopt": "~3.0.6"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/grunt-cli": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz",
+ "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==",
+ "dev": true,
+ "dependencies": {
+ "grunt-known-options": "~2.0.0",
+ "interpret": "~1.1.0",
+ "liftup": "~3.0.1",
+ "nopt": "~4.0.1",
+ "v8flags": "~3.2.0"
+ },
+ "bin": {
+ "grunt": "bin/grunt"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-cli/node_modules/nopt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz",
+ "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/grunt-contrib-clean": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz",
+ "integrity": "sha512-uRvnXfhiZt8akb/ZRDHJpQQtkkVkqc/opWO4Po/9ehC2hPxgptB9S6JHDC/Nxswo4CJSM0iFPT/Iym3cEMWzKA==",
+ "dev": true,
+ "dependencies": {
+ "async": "^3.2.3",
+ "rimraf": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "grunt": ">=0.4.5"
+ }
+ },
+ "node_modules/grunt-contrib-clean/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/grunt-contrib-compress": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-compress/-/grunt-contrib-compress-2.0.0.tgz",
+ "integrity": "sha512-r/dAGx4qG+rmBFF4lb/hTktW2huGMGxkSLf9msh3PPtq0+cdQRQerZJ30UKevX3BLQsohwLzO0p1z/LrH6aKXQ==",
+ "dev": true,
+ "dependencies": {
+ "adm-zip": "^0.5.1",
+ "archiver": "^5.1.0",
+ "chalk": "^4.1.0",
+ "lodash": "^4.17.20",
+ "pretty-bytes": "^5.4.1",
+ "stream-buffers": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10.16"
+ }
+ },
+ "node_modules/grunt-contrib-compress/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-contrib-compress/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/grunt-contrib-compress/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-contrib-compress/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-contrib-compress/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-contrib-compress/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-contrib-copy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz",
+ "integrity": "sha512-gFRFUB0ZbLcjKb67Magz1yOHGBkyU6uL29hiEW1tdQ9gQt72NuMKIy/kS6dsCbV0cZ0maNCb0s6y+uT1FKU7jA==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^1.1.1",
+ "file-sync-cmp": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-copy/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/grunt-contrib-uglify": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-5.2.2.tgz",
+ "integrity": "sha512-ITxiWxrjjP+RZu/aJ5GLvdele+sxlznh+6fK9Qckio5ma8f7Iv8woZjRkGfafvpuygxNefOJNc+hfjjBayRn2Q==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "maxmin": "^3.0.0",
+ "uglify-js": "^3.16.1",
+ "uri-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-contrib-uglify/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-contrib-watch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz",
+ "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==",
+ "dev": true,
+ "dependencies": {
+ "async": "^2.6.0",
+ "gaze": "^1.1.0",
+ "lodash": "^4.17.10",
+ "tiny-lr": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-contrib-watch/node_modules/async": {
+ "version": "2.6.4",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
+ "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/grunt-known-options": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz",
+ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz",
+ "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==",
+ "dev": true,
+ "dependencies": {
+ "colors": "~1.1.2",
+ "grunt-legacy-log-utils": "~2.1.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz",
+ "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "~4.1.0",
+ "lodash": "~4.17.19"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-log-utils/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-legacy-util": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz",
+ "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==",
+ "dev": true,
+ "dependencies": {
+ "async": "~3.2.0",
+ "exit": "~0.1.2",
+ "getobject": "~1.0.0",
+ "hooker": "~0.2.3",
+ "lodash": "~4.17.21",
+ "underscore.string": "~3.3.5",
+ "which": "~2.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/grunt-shell": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/grunt-shell/-/grunt-shell-4.0.0.tgz",
+ "integrity": "sha512-dHFy8VZDfWGYLTeNvIHze4PKXGvIlDWuN0UE7hUZstTQeiEyv1VmW1MaDYQ3X5tE3bCi3bEia1gGKH8z/f1czQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^3.0.0",
+ "npm-run-path": "^2.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ },
+ "peerDependencies": {
+ "grunt": ">=1"
+ }
+ },
+ "node_modules/grunt-shell/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/grunt-shell/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-shell/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/grunt-shell/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/grunt-shell/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-shell/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/grunt-sync": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/grunt-sync/-/grunt-sync-0.8.2.tgz",
+ "integrity": "sha512-PB+xKI9YPyZn3NZQXpKHfZVlxHdf1L8GMl+Wi0mLhYypWuOdZPW2EzTmSuhhFbXjkb0aIOxvII1zdZZEl9zqGg==",
+ "dev": true,
+ "dependencies": {
+ "fs-extra": "^6.0.1",
+ "glob": "^7.0.5",
+ "md5-file": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=6 <7 || >=8"
+ }
+ },
+ "node_modules/grunt-sync/node_modules/fs-extra": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
+ "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "node_modules/grunt-sync/node_modules/jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+ "dev": true,
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/grunt-sync/node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/grunt/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/grunt/node_modules/glob": {
+ "version": "7.1.7",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
+ "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/grunt/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/grunt/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/grunt/node_modules/minimatch": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+ "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/grunt/node_modules/nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ }
+ },
+ "node_modules/grunt/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/gzip-size": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz",
+ "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==",
+ "dev": true,
+ "dependencies": {
+ "duplexer": "^0.1.1",
+ "pify": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
+ },
+ "node_modules/helmet": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-6.2.0.tgz",
+ "integrity": "sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hooker": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz",
+ "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/hpp": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz",
+ "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==",
+ "dependencies": {
+ "lodash": "^4.17.12",
+ "type-is": "^1.6.12"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==",
+ "dev": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
+ "node_modules/interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==",
+ "dev": true
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-absolute": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz",
+ "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==",
+ "dev": true,
+ "dependencies": {
+ "is-relative": "^1.0.0",
+ "is-windows": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+ "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-expression": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz",
+ "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==",
+ "dependencies": {
+ "acorn": "^7.1.1",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/is-expression/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-relative": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz",
+ "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==",
+ "dev": true,
+ "dependencies": {
+ "is-unc-path": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-unc-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz",
+ "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==",
+ "dev": true,
+ "dependencies": {
+ "unc-path-regex": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
+ "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
+ "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz",
+ "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/core": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "import-local": "^3.0.2",
+ "jest-cli": "^28.1.3"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz",
+ "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==",
+ "dev": true,
+ "dependencies": {
+ "execa": "^5.0.0",
+ "p-limit": "^3.1.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz",
+ "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/expect": "^28.1.3",
+ "@jest/test-result": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^0.7.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^28.1.3",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-runtime": "^28.1.3",
+ "jest-snapshot": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "p-limit": "^3.1.0",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-circus/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-circus/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-circus/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-circus/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-circus/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz",
+ "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/core": "^28.1.3",
+ "@jest/test-result": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "import-local": "^3.0.2",
+ "jest-config": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jest-validate": "^28.1.3",
+ "prompts": "^2.0.1",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-cli/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-cli/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-cli/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-cli/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-cli/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-cli/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz",
+ "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "babel-jest": "^28.1.3",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^28.1.3",
+ "jest-environment-node": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.3",
+ "jest-runner": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jest-validate": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-config/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-config/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-config/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-config/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-config/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-diff": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz",
+ "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-diff/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-diff/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-diff/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-diff/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-diff/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-diff/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz",
+ "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==",
+ "dev": true,
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz",
+ "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-each/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-each/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-each/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-each/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-each/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-each/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz",
+ "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz",
+ "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz",
+ "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz",
+ "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==",
+ "dev": true,
+ "dependencies": {
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz",
+ "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-matcher-utils/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-matcher-utils/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz",
+ "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^28.1.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-message-util/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz",
+ "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
+ "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz",
+ "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz",
+ "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^28.1.3",
+ "jest-validate": "^28.1.3",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^1.1.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz",
+ "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==",
+ "dev": true,
+ "dependencies": {
+ "jest-regex-util": "^28.0.2",
+ "jest-snapshot": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-resolve/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-resolve/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz",
+ "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/console": "^28.1.3",
+ "@jest/environment": "^28.1.3",
+ "@jest/test-result": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^28.1.1",
+ "jest-environment-node": "^28.1.3",
+ "jest-haste-map": "^28.1.3",
+ "jest-leak-detector": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-resolve": "^28.1.3",
+ "jest-runtime": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jest-watcher": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "p-limit": "^3.1.0",
+ "source-map-support": "0.5.13"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-runner/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-runner/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-runner/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz",
+ "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/globals": "^28.1.3",
+ "@jest/source-map": "^28.1.2",
+ "@jest/test-result": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "execa": "^5.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.3",
+ "jest-snapshot": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-runtime/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-runtime/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz",
+ "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/traverse": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^28.1.3",
+ "@jest/transform": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/babel__traverse": "^7.0.6",
+ "@types/prettier": "^2.1.5",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^28.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^28.1.3",
+ "jest-get-type": "^28.0.2",
+ "jest-haste-map": "^28.1.3",
+ "jest-matcher-utils": "^28.1.3",
+ "jest-message-util": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^28.1.3",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-snapshot/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-snapshot/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/jest-sonar-reporter": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz",
+ "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==",
+ "dev": true,
+ "dependencies": {
+ "xml": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz",
+ "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-util/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-util/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-util/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-util/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-validate": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz",
+ "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "leven": "^3.1.0",
+ "pretty-format": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-validate/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-validate/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-validate/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-validate/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz",
+ "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==",
+ "dev": true,
+ "dependencies": {
+ "@jest/test-result": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "jest-util": "^28.1.3",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/jest-watcher/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-watcher/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
+ "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-stringify": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
+ "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonschema": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
+ "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz",
+ "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash": "^4.17.21",
+ "ms": "^2.1.1",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/jstransformer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz",
+ "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==",
+ "dependencies": {
+ "is-promise": "^2.0.0",
+ "promise": "^7.0.1"
+ }
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/kuler": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
+ },
+ "node_modules/lazystream": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
+ "dependencies": {
+ "readable-stream": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.6.3"
+ }
+ },
+ "node_modules/lazystream/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/lazystream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/libpq": {
+ "version": "1.8.12",
+ "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.12.tgz",
+ "integrity": "sha512-4lUY9BD9suz76mVS0kH4rRgRy620g/c9YZH5GYC3smfIpjtj6KiPuQ4IwQSHSZMMMhMM3tBFrYUrw8mHOOZVeg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "bindings": "1.5.0",
+ "nan": "^2.14.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/liftup": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz",
+ "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==",
+ "dev": true,
+ "dependencies": {
+ "extend": "^3.0.2",
+ "findup-sync": "^4.0.0",
+ "fined": "^1.2.0",
+ "flagged-respawn": "^1.0.1",
+ "is-plain-object": "^2.0.4",
+ "object.map": "^1.0.1",
+ "rechoir": "^0.7.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/liftup/node_modules/findup-sync": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz",
+ "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "micromatch": "^4.0.2",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/listenercount": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
+ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
+ },
+ "node_modules/livereload-js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz",
+ "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==",
+ "dev": true
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true
+ },
+ "node_modules/lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
+ },
+ "node_modules/lodash.difference": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
+ "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
+ },
+ "node_modules/lodash.escaperegexp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true
+ },
+ "node_modules/lodash.groupby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
+ "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
+ },
+ "node_modules/lodash.isfunction": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
+ "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
+ },
+ "node_modules/lodash.isnil": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
+ "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isundefined": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
+ "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="
+ },
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "dev": true
+ },
+ "node_modules/lodash.union": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
+ "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
+ },
+ "node_modules/logform": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
+ "integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==",
+ "dependencies": {
+ "@colors/colors": "1.5.0",
+ "@types/triple-beam": "^1.3.2",
+ "fecha": "^4.2.0",
+ "ms": "^2.1.1",
+ "safe-stable-stringify": "^2.3.1",
+ "triple-beam": "^1.3.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/luxon": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
+ "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+ "dev": true
+ },
+ "node_modules/make-iterator": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz",
+ "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/map-cache": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
+ "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/maxmin": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz",
+ "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "figures": "^3.2.0",
+ "gzip-size": "^5.1.1",
+ "pretty-bytes": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/maxmin/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/maxmin/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/maxmin/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/maxmin/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/maxmin/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/maxmin/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/md5-file": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-2.0.7.tgz",
+ "integrity": "sha512-kWAICpJv8fIY0Ka/90iOX9nCJ407Fgj82ceWwcxi2HvVkKGHRMS/Y4caqBaju5skNYXiQohGUjwGZ7rVgzUhRw==",
+ "dev": true
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
+ "node_modules/moment": {
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moment-timezone": {
+ "version": "0.5.43",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
+ "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/morgan": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
+ "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
+ "dependencies": {
+ "basic-auth": "~2.0.1",
+ "debug": "2.6.9",
+ "depd": "~2.0.0",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/morgan/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/morgan/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/morgan/node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/nan": {
+ "version": "2.17.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
+ "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==",
+ "dev": true,
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-abi": {
+ "version": "3.45.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.45.0.tgz",
+ "integrity": "sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-abi/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/node-addon-api": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
+ },
+ "node_modules/node-fetch": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
+ "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+ "dev": true
+ },
+ "node_modules/nodeman": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/nodeman/-/nodeman-1.1.2.tgz",
+ "integrity": "sha512-RpRpDnMI4TN/EPX6+my7f02585I14AQO8YrHTkO1aTiC65+sMa5G3NwQplN6kwue1njxWiaSz2UwCnDbwkLgeQ==",
+ "dev": true,
+ "dependencies": {
+ "colors": "*"
+ },
+ "bin": {
+ "_nodeman": "bin/_nodeman",
+ "nodeman": "bin/nodeman"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
+ "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
+ "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "node_modules/oauth": {
+ "version": "0.9.15",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
+ "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.defaults": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz",
+ "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==",
+ "dev": true,
+ "dependencies": {
+ "array-each": "^1.0.1",
+ "array-slice": "^1.0.0",
+ "for-own": "^1.0.0",
+ "isobject": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz",
+ "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==",
+ "dev": true,
+ "dependencies": {
+ "for-own": "^1.0.0",
+ "make-iterator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object.pick": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
+ "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/one-time": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+ "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+ "dependencies": {
+ "fn.name": "1.x.x"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+ "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "dev": true,
+ "dependencies": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/packet-reader": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
+ "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-filepath": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
+ "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==",
+ "dev": true,
+ "dependencies": {
+ "is-absolute": "^1.0.0",
+ "map-cache": "^0.2.0",
+ "path-root": "^0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-srcset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz",
+ "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/passport": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/passport/-/passport-0.5.3.tgz",
+ "integrity": "sha512-gGc+70h4gGdBWNsR3FuV3byLDY6KBTJAIExGFXTpQaYfbbcHCBlRRKx7RBQSpqEqc5Hh2qVzRs7ssvSfOpkUEA==",
+ "dependencies": {
+ "passport-strategy": "1.x.x",
+ "pause": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-google-oauth2": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/passport-google-oauth2/-/passport-google-oauth2-0.2.0.tgz",
+ "integrity": "sha512-62EdPtbfVdc55nIXi0p1WOa/fFMM8v/M8uQGnbcXA4OexZWCnfsEi3wo2buag+Is5oqpuHzOtI64JpHk0Xi5RQ==",
+ "dependencies": {
+ "passport-oauth2": "^1.1.2"
+ }
+ },
+ "node_modules/passport-google-oauth20": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
+ "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==",
+ "dependencies": {
+ "passport-oauth2": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/passport-local": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
+ "integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
+ "dependencies": {
+ "passport-strategy": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/passport-oauth2": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
+ "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
+ "dependencies": {
+ "base64url": "3.x.x",
+ "oauth": "0.9.x",
+ "passport-strategy": "1.x.x",
+ "uid2": "0.0.x",
+ "utils-merge": "1.x.x"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/jaredhanson"
+ }
+ },
+ "node_modules/passport-strategy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
+ "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/path": {
+ "version": "0.12.7",
+ "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
+ "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
+ "dependencies": {
+ "process": "^0.11.1",
+ "util": "^0.10.3"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/path-root": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz",
+ "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==",
+ "dev": true,
+ "dependencies": {
+ "path-root-regex": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-root-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz",
+ "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pause": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
+ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
+ },
+ "node_modules/pg": {
+ "version": "8.11.1",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz",
+ "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==",
+ "dependencies": {
+ "buffer-writer": "2.0.0",
+ "packet-reader": "1.0.0",
+ "pg-connection-string": "^2.6.1",
+ "pg-pool": "^3.6.1",
+ "pg-protocol": "^1.6.0",
+ "pg-types": "^2.1.0",
+ "pgpass": "1.x"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.1.1"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
+ "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz",
+ "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg=="
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-native": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/pg-native/-/pg-native-3.0.1.tgz",
+ "integrity": "sha512-LBVNWkNh0fVx/cienARRP2y22J5OpUsKBe0TpxzAx3arEUUdIs77aLSAHS3scS7SMaqc+OkG40CEu5fN0/cjIw==",
+ "dependencies": {
+ "libpq": "^1.8.10",
+ "pg-types": "^1.12.1",
+ "readable-stream": "1.0.31"
+ }
+ },
+ "node_modules/pg-native/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
+ },
+ "node_modules/pg-native/node_modules/pg-types": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.13.0.tgz",
+ "integrity": "sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ==",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~1.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.0",
+ "postgres-interval": "^1.1.0"
+ }
+ },
+ "node_modules/pg-native/node_modules/postgres-array": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.3.tgz",
+ "integrity": "sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg-native/node_modules/postgres-bytea": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg-native/node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg-native/node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg-native/node_modules/readable-stream": {
+ "version": "1.0.31",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz",
+ "integrity": "sha512-tco/Dwv1f/sgIgN6CWdj/restacPKNskK6yps1981ivH2ZmLYcs5o5rVzL3qaO/cSkhN8hYOMWs7+glzOLSgRg==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/pg-native/node_modules/string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="
+ },
+ "node_modules/pg-numeric": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
+ "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
+ "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
+ "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q=="
+ },
+ "node_modules/pg-types": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz",
+ "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "pg-numeric": "1.0.2",
+ "postgres-array": "~3.0.1",
+ "postgres-bytea": "~3.0.0",
+ "postgres-date": "~2.0.1",
+ "postgres-interval": "^3.0.0",
+ "postgres-range": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pg/node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pg/node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pg/node_modules/postgres-bytea": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+ "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg/node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pg/node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+ "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
+ "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
+ "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
+ "dependencies": {
+ "obuf": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz",
+ "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
+ "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/postgres-range": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz",
+ "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g=="
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
+ "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-bytes": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
+ "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
+ "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "node_modules/promise": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
+ "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
+ "dependencies": {
+ "asap": "~2.0.3"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "node_modules/pug": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.2.tgz",
+ "integrity": "sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==",
+ "dependencies": {
+ "pug-code-gen": "^3.0.2",
+ "pug-filters": "^4.0.0",
+ "pug-lexer": "^5.0.1",
+ "pug-linker": "^4.0.0",
+ "pug-load": "^3.0.0",
+ "pug-parser": "^6.0.0",
+ "pug-runtime": "^3.0.1",
+ "pug-strip-comments": "^2.0.0"
+ }
+ },
+ "node_modules/pug-attrs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz",
+ "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==",
+ "dependencies": {
+ "constantinople": "^4.0.1",
+ "js-stringify": "^1.0.2",
+ "pug-runtime": "^3.0.0"
+ }
+ },
+ "node_modules/pug-code-gen": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.2.tgz",
+ "integrity": "sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==",
+ "dependencies": {
+ "constantinople": "^4.0.1",
+ "doctypes": "^1.1.0",
+ "js-stringify": "^1.0.2",
+ "pug-attrs": "^3.0.0",
+ "pug-error": "^2.0.0",
+ "pug-runtime": "^3.0.0",
+ "void-elements": "^3.1.0",
+ "with": "^7.0.0"
+ }
+ },
+ "node_modules/pug-error": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.0.0.tgz",
+ "integrity": "sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ=="
+ },
+ "node_modules/pug-filters": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz",
+ "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==",
+ "dependencies": {
+ "constantinople": "^4.0.1",
+ "jstransformer": "1.0.0",
+ "pug-error": "^2.0.0",
+ "pug-walk": "^2.0.0",
+ "resolve": "^1.15.1"
+ }
+ },
+ "node_modules/pug-lexer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz",
+ "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==",
+ "dependencies": {
+ "character-parser": "^2.2.0",
+ "is-expression": "^4.0.0",
+ "pug-error": "^2.0.0"
+ }
+ },
+ "node_modules/pug-linker": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz",
+ "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==",
+ "dependencies": {
+ "pug-error": "^2.0.0",
+ "pug-walk": "^2.0.0"
+ }
+ },
+ "node_modules/pug-load": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz",
+ "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==",
+ "dependencies": {
+ "object-assign": "^4.1.1",
+ "pug-walk": "^2.0.0"
+ }
+ },
+ "node_modules/pug-parser": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz",
+ "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==",
+ "dependencies": {
+ "pug-error": "^2.0.0",
+ "token-stream": "1.0.0"
+ }
+ },
+ "node_modules/pug-runtime": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz",
+ "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg=="
+ },
+ "node_modules/pug-strip-comments": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz",
+ "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==",
+ "dependencies": {
+ "pug-error": "^2.0.0"
+ }
+ },
+ "node_modules/pug-walk": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz",
+ "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ=="
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
+ "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
+ },
+ "node_modules/random-bytes": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
+ "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdir-glob": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
+ "dependencies": {
+ "minimatch": "^5.1.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/readdir-glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz",
+ "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.9.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/redis": {
+ "version": "4.6.7",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.7.tgz",
+ "integrity": "sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw==",
+ "dependencies": {
+ "@redis/bloom": "1.2.0",
+ "@redis/client": "1.5.8",
+ "@redis/graph": "1.1.0",
+ "@redis/json": "1.0.4",
+ "@redis/search": "1.1.3",
+ "@redis/time-series": "1.0.4"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz",
+ "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==",
+ "dev": true,
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "dev": true
+ },
+ "node_modules/regenerator-transform": {
+ "version": "0.15.1",
+ "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz",
+ "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.8.4"
+ }
+ },
+ "node_modules/regexp-tree": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+ "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+ "dev": true,
+ "bin": {
+ "regexp-tree": "bin/regexp-tree"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz",
+ "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/regjsgen": "^0.8.0",
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.1.0",
+ "regjsparser": "^0.9.1",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsparser": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz",
+ "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==",
+ "dev": true,
+ "dependencies": {
+ "jsesc": "~0.5.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+ "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz",
+ "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rndm": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
+ "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw=="
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/safe-json-parse": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz",
+ "integrity": "sha512-o0JmTu17WGUaUOHa1l0FPGXKBfijbxK6qoHzlkihsDXxzBHvJcA7zgviKR92Xs841rX9pK16unfphLq0/KqX7A==",
+ "dev": true
+ },
+ "node_modules/safe-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
+ "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
+ "dev": true,
+ "dependencies": {
+ "regexp-tree": "~0.1.1"
+ }
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
+ "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/sanitize-html": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
+ "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
+ "dependencies": {
+ "deepmerge": "^4.2.2",
+ "escape-string-regexp": "^4.0.0",
+ "htmlparser2": "^8.0.0",
+ "is-plain-object": "^5.0.0",
+ "parse-srcset": "^1.0.2",
+ "postcss": "^8.3.11"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/sanitize-html/node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/segfault-handler": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/segfault-handler/-/segfault-handler-1.3.0.tgz",
+ "integrity": "sha512-p7kVHo+4uoYkr0jmIiTBthwV5L2qmWtben/KDunDZ834mbos+tY+iO0//HpAJpOFSQZZ+wxKWuRo4DxV02B7Lg==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "bindings": "^1.2.1",
+ "nan": "^2.14.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
+ "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
+ "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.18.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/sharp/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp/node_modules/node-addon-api": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
+ "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA=="
+ },
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp/node_modules/tar-fs": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz",
+ "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==",
+ "dependencies": {
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ }
+ },
+ "node_modules/sharp/node_modules/tar-stream": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz",
+ "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/sharp/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/simple-swizzle/node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slugify": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
+ "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/socket.io": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz",
+ "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "base64id": "~2.0.0",
+ "cors": "~2.8.5",
+ "debug": "~4.3.2",
+ "engine.io": "~6.5.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
+ "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
+ "dependencies": {
+ "ws": "~8.11.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
+ "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
+ "dev": true
+ },
+ "node_modules/stack-trace": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+ "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stream-buffers": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz",
+ "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.15.5",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz",
+ "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==",
+ "dependencies": {
+ "fast-fifo": "^1.1.0",
+ "queue-tick": "^1.0.1"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-template": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz",
+ "integrity": "sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA=="
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz",
+ "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/swagger-jsdoc": {
+ "version": "6.2.8",
+ "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
+ "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
+ "dev": true,
+ "dependencies": {
+ "commander": "6.2.0",
+ "doctrine": "3.0.0",
+ "glob": "7.1.6",
+ "lodash.mergewith": "^4.6.2",
+ "swagger-parser": "^10.0.3",
+ "yaml": "2.0.0-1"
+ },
+ "bin": {
+ "swagger-jsdoc": "bin/swagger-jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/swagger-jsdoc/node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/swagger-parser": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
+ "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
+ "dev": true,
+ "dependencies": {
+ "@apidevtools/swagger-parser": "10.0.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar": {
+ "version": "6.1.15",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz",
+ "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-fs/node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/terminal-link": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
+ "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "supports-hyperlinks": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-hex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/tiny-lr": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz",
+ "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==",
+ "dev": true,
+ "dependencies": {
+ "body": "^5.1.0",
+ "debug": "^3.1.0",
+ "faye-websocket": "~0.10.0",
+ "livereload-js": "^2.3.0",
+ "object-assign": "^4.1.0",
+ "qs": "^6.4.0"
+ }
+ },
+ "node_modules/tiny-lr/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/tmp": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
+ "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
+ "dependencies": {
+ "rimraf": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/token-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz",
+ "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+ },
+ "node_modules/traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/triple-beam": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
+ "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
+ "engines": {
+ "node": ">= 14.0.0"
+ }
+ },
+ "node_modules/ts-jest": {
+ "version": "28.0.8",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz",
+ "integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==",
+ "dev": true,
+ "dependencies": {
+ "bs-logger": "0.x",
+ "fast-json-stable-stringify": "2.x",
+ "jest-util": "^28.0.0",
+ "json5": "^2.2.1",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "semver": "7.x",
+ "yargs-parser": "^21.0.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "@jest/types": "^28.0.0",
+ "babel-jest": "^28.0.0",
+ "jest": "^28.0.0",
+ "typescript": ">=4.3"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "@jest/types": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-jest/node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ts-jest/node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ts-jest/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/ts-node": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
+ "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
+ "dev": true,
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
+ "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig=="
+ },
+ "node_modules/tslint": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
+ "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
+ "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^4.0.1",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.3",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.13.0",
+ "tsutils": "^2.29.0"
+ },
+ "bin": {
+ "tslint": "bin/tslint"
+ },
+ "engines": {
+ "node": ">=4.8.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev"
+ }
+ },
+ "node_modules/tslint/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/tslint/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/tslint/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/tslint/node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/tslint/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/tslint/node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/tslint/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tslint/node_modules/tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev"
+ }
+ },
+ "node_modules/tsscmp": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
+ "engines": {
+ "node": ">=0.6.x"
+ }
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
+ "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/uglify-js": {
+ "version": "3.17.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz",
+ "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==",
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/uid-safe": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
+ "dependencies": {
+ "random-bytes": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uid2": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
+ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
+ },
+ "node_modules/unc-path-regex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz",
+ "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/underscore.string": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz",
+ "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "^1.1.1",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unzipper": {
+ "version": "0.10.14",
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
+ "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==",
+ "dependencies": {
+ "big-integer": "^1.6.17",
+ "binary": "~0.3.0",
+ "bluebird": "~3.4.1",
+ "buffer-indexof-polyfill": "~1.0.0",
+ "duplexer2": "~0.1.4",
+ "fstream": "^1.0.12",
+ "graceful-fs": "^4.2.2",
+ "listenercount": "~1.0.1",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "~1.0.4"
+ }
+ },
+ "node_modules/unzipper/node_modules/bluebird": {
+ "version": "3.4.7",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
+ "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
+ },
+ "node_modules/unzipper/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/unzipper/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uri-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz",
+ "integrity": "sha512-8pMuAn4KacYdGMkFaoQARicp4HSw24/DHOVKWqVRJ8LhhAwPPFpdGvdL9184JVmUwe7vz7Z9n6IqI6t5n2ELdg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/util/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+ "dev": true
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz",
+ "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/v8flags": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz",
+ "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.9.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz",
+ "integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "dev": true,
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
+ "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/winston": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz",
+ "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==",
+ "dependencies": {
+ "@colors/colors": "1.5.0",
+ "@dabh/diagnostics": "^2.0.2",
+ "async": "^3.2.3",
+ "is-stream": "^2.0.0",
+ "logform": "^2.4.0",
+ "one-time": "^1.0.0",
+ "readable-stream": "^3.4.0",
+ "safe-stable-stringify": "^2.3.1",
+ "stack-trace": "0.0.x",
+ "triple-beam": "^1.3.0",
+ "winston-transport": "^4.5.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/winston-transport": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
+ "integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
+ "dependencies": {
+ "logform": "^2.3.2",
+ "readable-stream": "^3.6.0",
+ "triple-beam": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 6.4.0"
+ }
+ },
+ "node_modules/with": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz",
+ "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==",
+ "dependencies": {
+ "@babel/parser": "^7.9.6",
+ "@babel/types": "^7.9.6",
+ "assert-never": "^1.2.1",
+ "babel-walk": "3.0.0-canary-5"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/write-file-atomic": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
+ "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.11.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
+ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+ "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
+ "dev": true
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
+ },
+ "node_modules/xss-filters": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/xss-filters/-/xss-filters-1.2.7.tgz",
+ "integrity": "sha512-KzcmYT/f+YzcYrYRqw6mXxd25BEZCxBQnf+uXTopQDIhrmiaLwO+f+yLsIvvNlPhYvgff8g3igqrBxYh9k8NbQ=="
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "2.0.0-1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
+ "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+ "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/z-schema": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
+ "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
+ "dev": true,
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.isequal": "^4.5.0",
+ "validator": "^13.7.0"
+ },
+ "bin": {
+ "z-schema": "bin/z-schema"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "commander": "^9.4.1"
+ }
+ },
+ "node_modules/z-schema/node_modules/commander": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+ "dev": true,
+ "optional": true,
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
+ "node_modules/zip-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
+ "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==",
+ "dependencies": {
+ "archiver-utils": "^2.1.0",
+ "compress-commons": "^4.1.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ }
+ }
+}
diff --git a/worklenz-backend/package.json b/worklenz-backend/package.json
new file mode 100644
index 00000000..db238e1b
--- /dev/null
+++ b/worklenz-backend/package.json
@@ -0,0 +1,147 @@
+{
+ "name": "worklenz-backend",
+ "version": "1.4.16",
+ "private": true,
+ "engines": {
+ "npm": ">=8.11.0",
+ "node": ">=16.13.0",
+ "yarn": "WARNING: Please use npm package manager instead of yarn"
+ },
+ "main": "build/bin/www",
+ "repository": "GITHUB_REPO_HERE",
+ "author": "worklenz.com",
+ "scripts": {
+ "start": "node ./build/bin/www",
+ "tcs": "grunt build:tsc",
+ "build": "grunt build",
+ "watch": "grunt watch",
+ "es": "esbuild `find src -type f -name '*.ts'` --platform=node --minify=true --watch=true --target=esnext --format=cjs --tsconfig=tsconfig.prod.json --outdir=dist",
+ "copy": "grunt copy",
+ "sonar": "sonar-scanner -Dproject.settings=sonar-project-dev.properties",
+ "tsc": "tsc",
+ "test": "jest --setupFiles dotenv/config",
+ "test:watch": "jest --watch --setupFiles dotenv/config"
+ },
+ "jestSonar": {
+ "reportPath": "coverage",
+ "reportFile": "test-reporter.xml",
+ "indent": 4
+ },
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.378.0",
+ "@aws-sdk/client-ses": "^3.378.0",
+ "@aws-sdk/s3-request-presigner": "^3.378.0",
+ "@aws-sdk/util-format-url": "^3.357.0",
+ "axios": "^1.6.0",
+ "bcrypt": "^5.1.0",
+ "bluebird": "^3.7.2",
+ "compression": "^1.7.4",
+ "connect-flash": "^0.1.1",
+ "connect-pg-simple": "^7.0.0",
+ "cookie-parser": "~1.4.4",
+ "cors": "^2.8.5",
+ "cron": "^2.4.0",
+ "csurf": "^1.11.0",
+ "debug": "^4.3.4",
+ "dotenv": "^16.3.1",
+ "exceljs": "^4.3.0",
+ "express": "^4.18.2",
+ "express-rate-limit": "^6.8.0",
+ "express-session": "^1.17.3",
+ "express-validator": "^6.15.0",
+ "helmet": "^6.2.0",
+ "hpp": "^0.2.3",
+ "http-errors": "^2.0.0",
+ "jsonschema": "^1.4.1",
+ "jsonwebtoken": "^9.0.1",
+ "lodash": "^4.17.21",
+ "mime-types": "^2.1.35",
+ "moment": "^2.29.4",
+ "moment-timezone": "^0.5.43",
+ "morgan": "^1.10.0",
+ "nanoid": "^3.3.6",
+ "passport": "^0.5.3",
+ "passport-google-oauth2": "^0.2.0",
+ "passport-google-oauth20": "^2.0.0",
+ "passport-local": "^1.0.0",
+ "path": "^0.12.7",
+ "pg": "^8.11.1",
+ "pg-native": "^3.0.1",
+ "pug": "^3.0.2",
+ "redis": "^4.6.7",
+ "sanitize-html": "^2.11.0",
+ "segfault-handler": "^1.3.0",
+ "sharp": "^0.32.6",
+ "slugify": "^1.6.6",
+ "socket.io": "^4.7.1",
+ "uglify-js": "^3.17.4",
+ "winston": "^3.10.0",
+ "xss-filters": "^1.2.7"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.22.9",
+ "@babel/preset-typescript": "^7.22.5",
+ "@types/bcrypt": "^5.0.0",
+ "@types/bluebird": "^3.5.38",
+ "@types/compression": "^1.7.2",
+ "@types/connect-flash": "^0.0.37",
+ "@types/cookie-parser": "^1.4.3",
+ "@types/cron": "^2.0.1",
+ "@types/csurf": "^1.11.2",
+ "@types/express": "^4.17.17",
+ "@types/express-brute": "^1.0.2",
+ "@types/express-brute-redis": "^0.0.4",
+ "@types/express-rate-limit": "^6.0.0",
+ "@types/express-session": "^1.17.7",
+ "@types/express-validator": "^3.0.0",
+ "@types/fs-extra": "^9.0.13",
+ "@types/hpp": "^0.2.2",
+ "@types/http-errors": "^1.8.2",
+ "@types/jest": "^28.1.8",
+ "@types/jsonwebtoken": "^9.0.2",
+ "@types/lodash": "^4.14.196",
+ "@types/mime-types": "^2.1.1",
+ "@types/morgan": "^1.9.4",
+ "@types/node": "^18.17.1",
+ "@types/passport": "^1.0.12",
+ "@types/passport-google-oauth20": "^2.0.11",
+ "@types/passport-local": "^1.0.35",
+ "@types/pg": "^8.10.2",
+ "@types/pug": "^2.0.6",
+ "@types/redis": "^4.0.11",
+ "@types/sanitize-html": "^2.9.0",
+ "@types/sharp": "^0.31.1",
+ "@types/socket.io": "^3.0.2",
+ "@types/swagger-jsdoc": "^6.0.1",
+ "@types/toobusy-js": "^0.5.2",
+ "@types/uglify-js": "^3.17.1",
+ "@types/xss-filters": "^0.0.27",
+ "@typescript-eslint/eslint-plugin": "^5.62.0",
+ "@typescript-eslint/parser": "^5.62.0",
+ "chokidar": "^3.5.3",
+ "esbuild": "^0.17.19",
+ "esbuild-envfile-plugin": "^1.0.5",
+ "esbuild-node-externals": "^1.8.0",
+ "eslint": "^8.45.0",
+ "eslint-plugin-security": "^1.7.1",
+ "fs-extra": "^10.1.0",
+ "grunt": "^1.6.1",
+ "grunt-cli": "^1.4.3",
+ "grunt-contrib-clean": "^2.0.1",
+ "grunt-contrib-compress": "^2.0.0",
+ "grunt-contrib-copy": "^1.0.0",
+ "grunt-contrib-uglify": "^5.2.2",
+ "grunt-contrib-watch": "^1.1.0",
+ "grunt-shell": "^4.0.0",
+ "grunt-sync": "^0.8.2",
+ "jest": "^28.1.3",
+ "jest-sonar-reporter": "^2.0.0",
+ "ncp": "^2.0.0",
+ "nodeman": "^1.1.2",
+ "swagger-jsdoc": "^6.2.8",
+ "ts-jest": "^28.0.8",
+ "ts-node": "^10.9.1",
+ "tslint": "^6.1.3",
+ "typescript": "^4.9.5"
+ }
+}
diff --git a/worklenz-backend/release b/worklenz-backend/release
new file mode 100644
index 00000000..c3398755
--- /dev/null
+++ b/worklenz-backend/release
@@ -0,0 +1 @@
+901
\ No newline at end of file
diff --git a/worklenz-backend/scss/style.scss b/worklenz-backend/scss/style.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/worklenz-backend/sonar-project.properties b/worklenz-backend/sonar-project.properties
new file mode 100644
index 00000000..4b7d0bff
--- /dev/null
+++ b/worklenz-backend/sonar-project.properties
@@ -0,0 +1,10 @@
+sonar.projectKey="PROJECT_KEY_HERE"
+sonar.projectVersion=1.0
+sonar.host.url="SONAR_HOST_URL_HERE"
+sonar.exclusions=node_modules/**, src/public/**, scss/**, test/**
+sonar.sources=src,build
+sonor.tests=test
+sonar.test.inclusions=**/*.spec.ts,**/*.spec.js
+sonar.token="SONAR_TOKEN_HERE"
+sonar.javascript.lcov.reportPaths=coverage/lcov.info
+sonar.testExecutionReportPaths=coverage/test-reporter.xml
diff --git a/worklenz-backend/src/app.ts b/worklenz-backend/src/app.ts
new file mode 100644
index 00000000..bd8e266b
--- /dev/null
+++ b/worklenz-backend/src/app.ts
@@ -0,0 +1,176 @@
+import createError from "http-errors";
+import express, {NextFunction, Request, Response} from "express";
+import path from "path";
+import cookieParser from "cookie-parser";
+import logger from "morgan";
+import helmet from "helmet";
+import compression from "compression";
+import passport from "passport";
+import csurf from "csurf";
+import rateLimit from "express-rate-limit";
+import cors from "cors";
+import uglify from "uglify-js";
+import flash from "connect-flash";
+import hpp from "hpp";
+
+import passportConfig from "./passport";
+import indexRouter from "./routes/index";
+import apiRouter from "./routes/apis";
+import authRouter from "./routes/auth";
+import emailTemplatesRouter from "./routes/email-templates";
+
+import public_router from "./routes/public";
+import {isInternalServer, isProduction} from "./shared/utils";
+import sessionMiddleware from "./middlewares/session-middleware";
+import {send_to_slack} from "./shared/slack";
+import {CSP_POLICIES} from "./shared/csp";
+import safeControllerFunction from "./shared/safe-controller-function";
+import AwsSesController from "./controllers/aws-ses-controller";
+
+const app = express();
+
+app.use(compression());
+app.use(helmet({crossOriginResourcePolicy: false, crossOriginEmbedderPolicy: false}));
+
+app.use((_req: Request, res: Response, next: NextFunction) => {
+ res.setHeader("X-XSS-Protection", "1; mode=block");
+ res.removeHeader("server");
+ next();
+});
+
+function isLoggedIn(req: Request, _res: Response, next: NextFunction) {
+ return req.user ? next() : next(createError(401));
+}
+
+passportConfig(passport);
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+require("pug").filters = {
+ /**
+ * ```pug
+ * script
+ * :minify_js
+ * // JavaScript Syntax
+ * ```
+ * @param {String} text
+ * @param {Object} options
+ */
+ minify_js(text: string) {
+ if (!text) return;
+ // return text;
+ return uglify.minify({"script.js": text}).code;
+ }
+};
+
+// view engine setup
+app.set("views", path.join(__dirname, "views"));
+app.set("view engine", "pug");
+
+app.use(logger("dev"));
+app.use(express.json({limit: "50mb"}));
+app.use(express.urlencoded({extended: false, limit: "50mb"}));
+// Prevent HTTP Parameter Pollution
+app.use(hpp());
+app.use(cookieParser(process.env.COOKIE_SECRET));
+
+app.use(cors({
+ origin: [`https://${process.env.HOSTNAME}`],
+ methods: "GET,PUT,POST,DELETE",
+ preflightContinue: false,
+ credentials: true
+}));
+
+app.post("/-/csp", (req: express.Request, res: express.Response) => {
+ send_to_slack({
+ type: "⚠️ CSP Report",
+ body: req.body
+ });
+ res.sendStatus(200);
+});
+
+app.post("/webhook/emails/bounce", safeControllerFunction(AwsSesController.handleBounceResponse));
+app.post("/webhook/emails/complaints", safeControllerFunction(AwsSesController.handleComplaintResponse));
+app.post("/webhook/emails/reply", safeControllerFunction(AwsSesController.handleReplies));
+
+app.use(flash());
+app.use(csurf({cookie: true}));
+
+app.use((req: Request, res: Response, next: NextFunction) => {
+ res.setHeader("Content-Security-Policy", CSP_POLICIES);
+ const token = req.csrfToken();
+ res.cookie("XSRF-TOKEN", token);
+ res.locals.csrf = token;
+ next();
+});
+
+if (isProduction()) {
+ app.get("*.js", (req, res, next) => {
+ if (req.header("Accept-Encoding")?.includes("br")) {
+ req.url = `${req.url}.br`;
+ res.set("Content-Encoding", "br");
+ res.set("Content-Type", "application/javascript; charset=UTF-8");
+ } else if (req.header("Accept-Encoding")?.includes("gzip")) {
+ req.url = `${req.url}.gz`;
+ res.set("Content-Encoding", "gzip");
+ res.set("Content-Type", "application/javascript; charset=UTF-8");
+ }
+ next();
+ });
+}
+
+app.use(express.static(path.join(__dirname, "public")));
+app.set("trust proxy", 1);
+app.use(sessionMiddleware);
+
+app.use(passport.initialize());
+app.use(passport.session());
+
+const apiLimiter = rateLimit({
+ windowMs: 15 * 60 * 1000, // 15 minutes
+ max: 1500, // Limit each IP to 2000 requests per `window` (here, per 15 minutes)
+ standardHeaders: false, // Return rate limit info in the `RateLimit-*` headers
+ legacyHeaders: false, // Disable the `X-RateLimit-*` headers
+});
+
+app.use((req, res, next) => {
+ const {send} = res;
+ res.send = function (obj) {
+ if (req.headers.accept?.includes("application/json"))
+ return send.call(this, `)]}',\n${JSON.stringify(obj)}`);
+ return send.call(this, obj);
+ };
+ next();
+});
+
+app.use("/secure", authRouter);
+app.use("/public", public_router);
+app.use("/api/v1", isLoggedIn, apiRouter);
+app.use("/", indexRouter);
+
+if (isInternalServer())
+ app.use("/email-templates", emailTemplatesRouter);
+
+
+// catch 404 and forward to error handler
+app.use((req: Request, res: Response) => {
+ res.locals.error_title = "404 Not Found.";
+ res.locals.error_message = `The requested URL ${req.url} was not found on this server.`;
+ res.locals.error_image = "/assets/images/404.webp";
+ res.status(400);
+ res.render("error");
+});
+
+// error handler
+app.use((err: { message: string; status: number; }, _req: Request, res: Response) => {
+ // set locals, only providing error in development
+ res.locals.error_title = "500 Internal Server Error.";
+ res.locals.error_message = "Oops, something went wrong.";
+ res.locals.error_message2 = "Try to refresh this page or feel free to contact us if the problem persists.";
+ res.locals.error_image = "/assets/images/500.png";
+
+ // render the error page
+ res.status(err.status || 500);
+ res.render("error");
+});
+
+export default app;
diff --git a/worklenz-backend/src/bin/config.ts b/worklenz-backend/src/bin/config.ts
new file mode 100644
index 00000000..c87b8127
--- /dev/null
+++ b/worklenz-backend/src/bin/config.ts
@@ -0,0 +1,6 @@
+import dotenv from "dotenv";
+import SegfaultHandler from "segfault-handler";
+
+dotenv.config();
+global.Promise = require("bluebird");
+SegfaultHandler.registerHandler("crash.log");
diff --git a/worklenz-backend/src/bin/www.ts b/worklenz-backend/src/bin/www.ts
new file mode 100644
index 00000000..9e2061c0
--- /dev/null
+++ b/worklenz-backend/src/bin/www.ts
@@ -0,0 +1,119 @@
+#!/usr/bin/env node
+
+// config should be imported at the top of this file.
+import "./config";
+
+import {Server, Socket} from "socket.io";
+import http, {IncomingHttpHeaders} from "http";
+
+import app from "../app";
+import {register} from "../socket.io";
+import {IO} from "../shared/io";
+import sessionMiddleware from "../middlewares/session-middleware";
+import {getLoggedInUserIdFromSocket} from "../socket.io/util";
+import {startCronJobs} from "../cron_jobs";
+import FileConstants from "../shared/file-constants";
+import {initRedis} from "../redis/client";
+import DbTaskStatusChangeListener from "../pg_notify_listeners/db-task-status-changed";
+
+function normalizePort(val?: string) {
+ const p = parseInt(val || "0", 10);
+ if (isNaN(p)) return val; // named pipe
+ if (p >= 0) return p; // port number
+ return false;
+}
+
+const port = normalizePort(process.env.PORT);
+app.set("port", port);
+
+const server = http.createServer(app);
+
+const io = new Server(server, {
+ transports: ["websocket"],
+ path: "/socket",
+ cors: {
+ origin: (process.env.SOCKET_IO_CORS || "*").split(",")
+ },
+ cookie: true
+});
+
+const wrap = (middleware: any) => (socket: any, next: any) => middleware(socket.request, {}, next);
+
+io.use(wrap(sessionMiddleware));
+
+io.use((socket, next) => {
+ const userId = getLoggedInUserIdFromSocket(socket);
+ if (userId)
+ return next();
+ return next(new Error("401 unauthorized"));
+});
+
+io.engine.on("initial_headers", (headers: IncomingHttpHeaders) => {
+ headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains";
+ headers["X-Content-Type-Options"] = "nosniff";
+ headers["X-Frame-Options"] = "Deny";
+ headers["X-XSS-Protection"] = "1; mode=block";
+});
+
+io.on("connection", (socket: Socket) => {
+ register(io, socket);
+});
+
+IO.setInstance(io);
+
+function onError(error: any) {
+ DbTaskStatusChangeListener.disconnect();
+
+ if (error.syscall !== "listen") {
+ throw error;
+ }
+
+ const bind = typeof port === "string"
+ ? `Pipe ${port}`
+ : `Port ${port}`;
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case "EACCES":
+ console.error(`${bind} requires elevated privileges`);
+ process.exit(1);
+ break;
+ case "EADDRINUSE":
+ console.error(`${bind} is already in use`);
+ process.exit(1);
+ break;
+ default:
+ throw error;
+ }
+}
+
+function onListening() {
+ const addr = server.address();
+ if (!addr) return;
+
+ const bind = typeof addr === "string"
+ ? `pipe ${addr}`
+ : `port ${addr.port}`;
+
+ startCronJobs();
+ // TODO - uncomment initRedis()
+ // void initRedis();
+ FileConstants.init();
+ void DbTaskStatusChangeListener.connect();
+
+ console.info(`Listening on ${bind}`);
+}
+
+function onClose() {
+ DbTaskStatusChangeListener.disconnect();
+}
+
+server.on("error", onError);
+server.on("close", onClose);
+server.on("listening", onListening);
+
+process.on("SIGINT", () => {
+ server.close();
+});
+
+server.listen(port);
diff --git a/worklenz-backend/src/config/db-config.ts b/worklenz-backend/src/config/db-config.ts
new file mode 100644
index 00000000..c9e3151b
--- /dev/null
+++ b/worklenz-backend/src/config/db-config.ts
@@ -0,0 +1,9 @@
+export default {
+ user: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_NAME,
+ host: process.env.DB_HOST,
+ port: +(process.env.DB_PORT as string),
+ max: +(process.env.DB_MAX_CLIENTS as string),
+ idleTimeoutMillis: 30000,
+};
diff --git a/worklenz-backend/src/config/db.ts b/worklenz-backend/src/config/db.ts
new file mode 100644
index 00000000..fd15e0d9
--- /dev/null
+++ b/worklenz-backend/src/config/db.ts
@@ -0,0 +1,15 @@
+import pgModule, {QueryResult} from "pg";
+import dbConfig from "./db-config";
+
+const pg = (process.env.USE_PG_NATIVE === "true" && pgModule.native) ? pgModule.native : pgModule;
+const pool = new pg.Pool(dbConfig);
+
+pool.on("error", (err: Error) => {
+ // eslint-disable-next-line no-console
+ console.error("pg idle client error", err, err.message, err.stack);
+});
+
+export default {
+ pool,
+ query: (text: string, params?: unknown[]) => pool.query(text, params) as Promise>,
+};
diff --git a/worklenz-backend/src/controllers/.gitkeep b/worklenz-backend/src/controllers/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/worklenz-backend/src/controllers/access-controls-controller.ts b/worklenz-backend/src/controllers/access-controls-controller.ts
new file mode 100644
index 00000000..3db639e0
--- /dev/null
+++ b/worklenz-backend/src/controllers/access-controls-controller.ts
@@ -0,0 +1,15 @@
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+
+export default class AccessControlsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getRoles(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, default_role, admin_role FROM roles WHERE team_id = $1 AND owner IS FALSE ORDER BY name;`;
+ const result = await db.query(q, [req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/activity-logs-controller.ts b/worklenz-backend/src/controllers/activity-logs-controller.ts
new file mode 100644
index 00000000..4e1e5960
--- /dev/null
+++ b/worklenz-backend/src/controllers/activity-logs-controller.ts
@@ -0,0 +1,34 @@
+import moment from "moment";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {formatDuration, formatLogText, getColor} from "../shared/utils";
+
+export default class ActivitylogsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {id} = req.params;
+ const q = `SELECT get_activity_logs_by_task($1) AS activity_logs;`;
+ const result = await db.query(q, [id]);
+ const [data] = result.rows;
+
+ for (const log of data.activity_logs.logs) {
+ if (log.attribute_type === "estimation") {
+ log.previous = formatDuration(moment.duration(log.previous, "minutes"));
+ log.current = formatDuration(moment.duration(log.current, "minutes"));
+ }
+ if (log.assigned_user) log.assigned_user.color_code = getColor(log.assigned_user.name);
+ log.done_by.color_code = getColor(log.done_by.name);
+ log.log_text = await formatLogText(log);
+ log.attribute_type = log.attribute_type?.replace(/_/g, " ");
+ }
+ data.activity_logs.color_code = getColor(data.activity_logs.name);
+
+ return res.status(200).send(new ServerResponse(true, data.activity_logs));
+ }
+}
diff --git a/worklenz-backend/src/controllers/admin-center-controller.ts b/worklenz-backend/src/controllers/admin-center-controller.ts
new file mode 100644
index 00000000..48e6e317
--- /dev/null
+++ b/worklenz-backend/src/controllers/admin-center-controller.ts
@@ -0,0 +1,309 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getColor} from "../shared/utils";
+import {calculateStorage} from "../shared/s3";
+import {NotificationsService} from "../services/notifications/notifications.service";
+import {SocketEvents} from "../socket.io/events";
+import {IO} from "../shared/io";
+
+export default class AdminCenterController extends WorklenzControllerBase {
+
+ public static async checkIfUserActiveInOtherTeams(owner_id: string, email: string) {
+ if (!owner_id) throw new Error("Owner not found.");
+
+ const q = `SELECT EXISTS(SELECT tmi.team_member_id
+ FROM team_member_info_view AS tmi
+ JOIN teams AS t ON tmi.team_id = t.id
+ JOIN team_members AS tm ON tmi.team_member_id = tm.id
+ WHERE tmi.email = $1::TEXT
+ AND t.user_id = $2::UUID AND tm.active = true);`;
+ const result = await db.query(q, [email, owner_id]);
+
+ const [data] = result.rows;
+ return data.exists;
+ }
+
+ // organization
+ @HandleExceptions()
+ public static async getOrganizationDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ // const q = `SELECT organization_name AS name,
+ // contact_number,
+ // contact_number_secondary,
+ // (SELECT email FROM users WHERE id = users_data.user_id),
+ // (SELECT name FROM users WHERE id = users_data.user_id) AS owner_name
+ // FROM users_data
+ // WHERE user_id = $1;`;
+ const q = `SELECT organization_name AS name,
+ contact_number,
+ contact_number_secondary,
+ (SELECT email FROM users WHERE id = organizations.user_id),
+ (SELECT name FROM users WHERE id = organizations.user_id) AS owner_name
+ FROM organizations
+ WHERE user_id = $1;`;
+ const result = await db.query(q, [req.user?.owner_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getOrganizationAdmins(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT u.name, email, owner AS is_owner
+ FROM users u
+ LEFT JOIN team_members tm ON u.id = tm.user_id
+ LEFT JOIN roles r ON tm.role_id = r.id
+ WHERE tm.team_id IN (SELECT id FROM teams WHERE teams.user_id = $1)
+ AND (admin_role IS TRUE OR owner IS TRUE)
+ GROUP BY u.name, email, owner
+ ORDER BY owner DESC, u.name;`;
+ const result = await db.query(q, [req.user?.owner_id]);
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getOrganizationUsers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["outer_tmiv.name", "outer_tmiv.email"]);
+
+ const q = `SELECT ROW_TO_JSON(rec) AS users
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT email,
+ STRING_AGG(DISTINCT CAST(user_id AS VARCHAR), ', ') AS user_id,
+ STRING_AGG(DISTINCT name, ', ') AS name,
+ STRING_AGG(DISTINCT avatar_url, ', ') AS avatar_url,
+ (SELECT twl.created_at
+ FROM task_work_log twl
+ WHERE twl.user_id IN (SELECT tmiv.user_id
+ FROM team_member_info_view tmiv
+ WHERE tmiv.email = outer_tmiv.email)
+ ORDER BY created_at DESC
+ LIMIT 1) AS last_logged
+ FROM team_member_info_view outer_tmiv
+ WHERE outer_tmiv.team_id IN (SELECT id
+ FROM teams
+ WHERE teams.user_id = $1) ${searchQuery}
+ GROUP BY email
+ ORDER BY email LIMIT $2 OFFSET $3) t) AS data
+ FROM (SELECT DISTINCT email
+ FROM team_member_info_view outer_tmiv
+ WHERE outer_tmiv.team_id IN
+ (SELECT id
+ FROM teams
+ WHERE teams.user_id = $1) ${searchQuery}) AS total) rec;`;
+ const result = await db.query(q, [req.user?.owner_id, size, offset]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data.users));
+ }
+
+ @HandleExceptions()
+ public static async updateOrganizationName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {name} = req.body;
+ // const q = `UPDATE users_data
+ // SET organization_name = $1
+ // WHERE user_id = $2;`;
+ const q = `UPDATE organizations
+ SET organization_name = $1
+ WHERE user_id = $2;`;
+ const result = await db.query(q, [name, req.user?.owner_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async updateOwnerContactNumber(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {contact_number} = req.body;
+ const q = `UPDATE organizations
+ SET contact_number = $1
+ WHERE user_id = $2;`;
+ const result = await db.query(q, [contact_number, req.user?.owner_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = ``;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getOrganizationTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, size, offset} = this.toPaginationOptions(req.query, ["name"]);
+
+ let size_changed = size;
+
+ if (offset == 0) size_changed = size_changed - 1;
+
+ const currentTeamClosure = offset == 0 ? `,
+ (SELECT COALESCE(ROW_TO_JSON(c), '{}'::JSON)
+ FROM (SELECT id,
+ name,
+ created_at,
+ (SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT CASE
+ WHEN u.name IS NOT NULL THEN u.name
+ ELSE (SELECT name
+ FROM email_invitations
+ WHERE team_member_id = team_members.id) END,
+ avatar_url
+ FROM team_members
+ LEFT JOIN users u on team_members.user_id = u.id
+ WHERE team_id = teams.id) rec) AS team_members
+ FROM teams
+ WHERE user_id = $1 AND teams.id = $4) c) AS current_team_data` : ``;
+
+ const q = `SELECT ROW_TO_JSON(rec) AS teams
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ created_at,
+ (SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT CASE
+ WHEN u.name IS NOT NULL THEN u.name
+ ELSE (SELECT name
+ FROM email_invitations
+ WHERE team_member_id = team_members.id) END,
+ avatar_url
+ FROM team_members
+ LEFT JOIN users u on team_members.user_id = u.id
+ WHERE team_id = teams.id) rec) AS team_members
+ FROM teams
+ WHERE user_id = $1 AND NOT teams.id = $4 ${searchQuery}
+ ORDER BY name, created_at
+ LIMIT $2 OFFSET $3) t) AS data
+ ${currentTeamClosure}
+ FROM teams
+ WHERE user_id = $1 ${searchQuery}) rec;`;
+ const result = await db.query(q, [req.user?.owner_id, size_changed, offset, req.user?.team_id]);
+
+ const [obj] = result.rows;
+
+ for (const team of obj.teams?.data || []) {
+ team.names = this.createTagList(team?.team_members);
+ team.names.map((a: any) => a.color_code = getColor(a.name));
+ }
+
+ if (obj.teams.current_team_data) {
+ obj.teams.current_team_data.names = this.createTagList(obj.teams.current_team_data?.team_members);
+ obj.teams.current_team_data.names.map((a: any) => a.color_code = getColor(a.name));
+ }
+
+ return res.status(200).send(new ServerResponse(true, obj.teams || {}));
+ }
+
+ @HandleExceptions()
+ public static async getTeamDetails(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {id} = req.params;
+
+ const q = `SELECT id,
+ name,
+ created_at,
+ (SELECT count(*) FROM team_members WHERE team_id = teams.id) as members_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT tm.id,
+ tm.user_id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT team_member_info_view.email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT team_member_info_view.avatar_url
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ role_id,
+ r.name AS role_name
+ FROM team_members tm
+ LEFT JOIN users u on tm.user_id = u.id
+ LEFT JOIN roles r on tm.role_id = r.id
+ WHERE tm.team_id = teams.id
+ ORDER BY r.name = 'Owner' DESC, u.name) rec) AS team_members
+ FROM teams
+ WHERE id = $1;`;
+ const result = await db.query(q, [id]);
+
+ const [obj] = result.rows;
+
+ obj.names = this.createTagList(obj?.team_members);
+ obj.names.map((a: any) => a.color_code = getColor(a.name));
+
+ return res.status(200).send(new ServerResponse(true, obj || {}));
+ }
+
+ @HandleExceptions()
+ public static async updateTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {id} = req.params;
+ const {name, teamMembers} = req.body;
+
+ const updateNameQuery = `UPDATE teams
+ SET name = $1
+ WHERE id = $2;`;
+ await db.query(updateNameQuery, [name, id]);
+
+ if (teamMembers.length) {
+ teamMembers.forEach(async (element: { role_name: string; user_id: string; }) => {
+ const q = `UPDATE team_members
+ SET role_id = (SELECT id FROM roles WHERE roles.team_id = $1 AND name = $2)
+ WHERE user_id = $3
+ AND team_id = $1;`;
+ await db.query(q, [id, element.role_name, element.user_id]);
+ });
+ }
+
+ return res.status(200).send(new ServerResponse(true, [], "Team updated successfully"));
+ }
+
+ @HandleExceptions()
+ public static async deleteTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {id} = req.params;
+
+ if (id == req.user?.team_id) {
+ return res.status(200).send(new ServerResponse(true, [], "Please switch to another team before attempting deletion.")
+ .withTitle("Unable to remove the presently active team!"));
+ }
+
+ const q = `DELETE FROM teams WHERE id = $1;`;
+ const result = await db.query(q, [id]);
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {id} = req.params;
+ const {teamId} = req.body;
+
+ if (!id || !teamId) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
+
+
+ const q = `SELECT remove_team_member($1, $2, $3) AS member;`;
+ const result = await db.query(q, [id, req.user?.id, teamId]);
+ const [data] = result.rows;
+
+ const message = `You have been removed from ${req.user?.team_name} by ${req.user?.name}`;
+
+ NotificationsService.sendNotification({
+ receiver_socket_id: data.socket_id,
+ message,
+ team: data.team,
+ team_id: id
+ });
+
+ IO.emitByUserId(data.member.id, req.user?.id || null, SocketEvents.TEAM_MEMBER_REMOVED, {
+ teamId: id,
+ message
+ });
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+
+}
diff --git a/worklenz-backend/src/controllers/attachment-controller.ts b/worklenz-backend/src/controllers/attachment-controller.ts
new file mode 100644
index 00000000..e8fef573
--- /dev/null
+++ b/worklenz-backend/src/controllers/attachment-controller.ts
@@ -0,0 +1,163 @@
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import { humanFileSize, log_error, smallId } from "../shared/utils";
+import { ServerResponse } from "../models/server-response";
+import {
+ createPresignedUrlWithClient,
+ deleteObject,
+ getAvatarKey,
+ getKey,
+ getRootDir,
+ uploadBase64,
+ uploadBuffer
+} from "../shared/s3";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+const {S3_URL} = process.env;
+
+if (!S3_URL) {
+ log_error("Invalid S3_URL. Please check .env file.");
+}
+
+export default class AttachmentController extends WorklenzControllerBase {
+
+ @HandleExceptions()
+ public static async createTaskAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { file, file_name, task_id, project_id, size, type } = req.body;
+
+ const q = `
+ INSERT INTO task_attachments (name, task_id, team_id, project_id, uploaded_by, size, type)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING id, name, size, type, created_at, CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url;
+ `;
+
+ const result = await db.query(q, [
+ file_name,
+ task_id,
+ req.user?.team_id,
+ project_id,
+ req.user?.id,
+ size,
+ type,
+ `${S3_URL}/${getRootDir()}`
+ ]);
+ const [data] = result.rows;
+
+ const s3Url = await uploadBase64(file, getKey(req.user?.team_id as string, project_id, data.id, data.type));
+
+ if (!data?.id || !s3Url)
+ return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
+
+ data.size = humanFileSize(data.size);
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async createAvatarAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { type, buffer } = req.body;
+
+ const s3Url = await uploadBuffer(buffer as Buffer, type, getAvatarKey(req.user?.id as string, type));
+
+ if (!s3Url)
+ return res.status(200).send(new ServerResponse(false, null, "Avatar upload failed"));
+
+ const q = "UPDATE users SET avatar_url = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING avatar_url;";
+ const result = await db.query(q, [req.user?.id, `${s3Url}?v=${smallId(4)}`]);
+ const [data] = result.rows;
+ if (!data)
+ return res.status(200).send(new ServerResponse(false, null, "Avatar upload failed"));
+
+ return res.status(200).send(new ServerResponse(true, { url: data.avatar_url }, "Avatar updated."));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id,
+ name,
+ size,
+ CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url,
+ type,
+ created_at
+ FROM task_attachments
+ WHERE task_id = $1;
+ `;
+ const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`]);
+
+ for (const item of result.rows)
+ item.size = humanFileSize(item.size);
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { size, offset } = this.toPaginationOptions(req.query, "name");
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS attachments
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT task_attachments.id,
+ task_attachments.name,
+ CONCAT((SELECT key FROM projects WHERE id = task_attachments.project_id), '-',
+ (SELECT task_no FROM tasks WHERE id = task_attachments.task_id)) AS task_key,
+ size,
+ CONCAT($2::TEXT, '/', task_attachments.team_id, '/', task_attachments.project_id, '/',task_attachments.id,'.',type) AS url,
+ task_attachments.type,
+ task_attachments.created_at,
+ t.name AS task_name,
+ (SELECT name FROM users WHERE id = task_attachments.uploaded_by) AS uploader_name
+ FROM task_attachments
+ LEFT JOIN tasks t ON task_attachments.task_id = t.id
+ WHERE task_attachments.project_id = $1
+ ORDER BY created_at DESC
+ LIMIT $3 OFFSET $4)t) AS data
+ FROM task_attachments
+ LEFT JOIN tasks t ON task_attachments.task_id = t.id
+ WHERE task_attachments.project_id = $1) rec;
+ `;
+ const result = await db.query(q, [req.params.id, `${S3_URL}/${getRootDir()}`, size, offset]);
+ const [data] = result.rows;
+
+ for (const item of data?.attachments.data || [])
+ item.size = humanFileSize(item.size);
+
+ return res.status(200).send(new ServerResponse(true, data?.attachments || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE
+ FROM task_attachments
+ WHERE id = $1
+ RETURNING CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key;`;
+ const result = await db.query(q, [req.params.id, getRootDir()]);
+ const [data] = result.rows;
+
+ if (data?.key)
+ void deleteObject(data.key);
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async download(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS key
+ FROM task_attachments
+ WHERE id = $1;`;
+ const result = await db.query(q, [req.query.id, getRootDir()]);
+ const [data] = result.rows;
+
+ if (data?.key) {
+ const url = await createPresignedUrlWithClient(data.key, req.query.file as string);
+ return res.status(200).send(new ServerResponse(true, url));
+ }
+
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+}
diff --git a/worklenz-backend/src/controllers/auth-controller.ts b/worklenz-backend/src/controllers/auth-controller.ts
new file mode 100644
index 00000000..1a5043ed
--- /dev/null
+++ b/worklenz-backend/src/controllers/auth-controller.ts
@@ -0,0 +1,141 @@
+import bcrypt from "bcrypt";
+
+import {sendResetEmail, sendResetSuccessEmail} from "../shared/email-templates";
+
+import {ServerResponse} from "../models/server-response";
+import {AuthResponse} from "../models/auth-response";
+
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import db from "../config/db";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {PasswordStrengthChecker} from "../shared/password-strength-check";
+import FileConstants from "../shared/file-constants";
+
+export default class AuthController extends WorklenzControllerBase {
+ /** This just send ok response to the client when the request came here through the sign-up-validator */
+ public static async status_check(_req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+
+ public static async checkPasswordStrength(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const result = PasswordStrengthChecker.validate(req.query.password as string);
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ public static verify(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ // Flash messages sent from passport-local-signup.ts and passport-local-login.ts
+ const errors = req.flash()["error"] || [];
+ const messages = req.flash()["success"] || [];
+ // If there are multiple messages, we will send one at a time.
+ const auth_error = errors.length > 0 ? errors[0] : null;
+ const message = messages.length > 0 ? messages[0] : null;
+
+ const midTitle = req.query.strategy === "login" ? "Login Failed!" : "Signup Failed!";
+ const title = req.query.strategy ? midTitle : null;
+
+ if (req.user)
+ req.user.build_v = FileConstants.getRelease();
+
+ return res.status(200).send(new AuthResponse(title, req.isAuthenticated(), req.user || null, auth_error, message));
+ }
+
+ public static logout(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ req.logout(() => true);
+ req.session.destroy(() => {
+ res.redirect("/");
+ });
+ }
+
+ private static async destroyOtherSessions(userId: string, sessionId: string) {
+ try {
+ const q = `DELETE FROM pg_sessions WHERE (sess ->> 'passport')::JSON ->> 'user'::TEXT = $1 AND sid != $2;`;
+ await db.query(q, [userId, sessionId]);
+ } catch (error) {
+ // ignored
+ }
+ }
+
+ @HandleExceptions()
+ public static async changePassword(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+
+ const currentPassword = req.body.password;
+ const newPassword = req.body.new_password;
+
+ const q = `SELECT id, email, google_id, password FROM users WHERE id = $1;`;
+ const result = await db.query(q, [req.user?.id || null]);
+ const [data] = result.rows;
+
+ if (data) {
+ // Compare the password
+ if (bcrypt.compareSync(currentPassword, data.password)) {
+ const salt = bcrypt.genSaltSync(10);
+ const encryptedPassword = bcrypt.hashSync(newPassword, salt);
+
+ const updatePasswordQ = `UPDATE users SET password = $1 WHERE id = $2;`;
+ await db.query(updatePasswordQ, [encryptedPassword, req.user?.id || null]);
+
+ if (req.user?.id)
+ AuthController.destroyOtherSessions(req.user.id, req.sessionID);
+
+ return res.status(200).send(new ServerResponse(true, null, "Password updated successfully!"));
+ }
+
+ return res.status(200).send(new ServerResponse(false, null, "Old password does not match!"));
+ }
+ }
+
+ @HandleExceptions({logWithError: "body"})
+ public static async reset_password(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const {email} = req.body;
+
+ const q = `SELECT id, email, google_id, password FROM users WHERE email = $1;`;
+ const result = await db.query(q, [email || null]);
+
+ if (!result.rowCount)
+ return res.status(200).send(new ServerResponse(false, null, "Account does not exists!"));
+
+ const [data] = result.rows;
+
+ if (data?.google_id) {
+ return res.status(200).send(new ServerResponse(false, null, "Password reset failed!"));
+ }
+
+ if (data?.password) {
+ const userIdBase64 = Buffer.from(data.id, "utf8").toString("base64");
+
+ const salt = bcrypt.genSaltSync(10);
+ const hashedUserData = bcrypt.hashSync(data.id + data.email + data.password, salt);
+ const hashedString = hashedUserData.toString().replace(/\//g, "-");
+
+ sendResetEmail(email, userIdBase64, hashedString);
+ return res.status(200).send(new ServerResponse(true, null, "Password reset email has been sent to your email. Please check your email."));
+ }
+ return res.status(200).send(new ServerResponse(false, null, "Email not found!"));
+ }
+
+ @HandleExceptions({logWithError: "body"})
+ public static async verify_reset_email(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const {user, hash, password} = req.body;
+ const hashedString = hash.replace(/\-/g, "/");
+
+ const userId = Buffer.from(user as string, "base64").toString("ascii");
+
+ const q = `SELECT id, email, google_id, password FROM users WHERE id = $1;`;
+ const result = await db.query(q, [userId || null]);
+ const [data] = result.rows;
+
+ const salt = bcrypt.genSaltSync(10);
+
+ if (bcrypt.compareSync(data.id + data.email + data.password, hashedString)) {
+ const encryptedPassword = bcrypt.hashSync(password, salt);
+ const updatePasswordQ = `UPDATE users SET password = $1 WHERE id = $2;`;
+ await db.query(updatePasswordQ, [encryptedPassword, userId || null]);
+
+ sendResetSuccessEmail(data.email);
+ return res.status(200).send(new ServerResponse(true, null, "Password updated successfully"));
+ }
+ return res.status(200).send(new ServerResponse(false, null, "Invalid Request. Please try again."));
+ }
+}
diff --git a/worklenz-backend/src/controllers/aws-ses-controller.ts b/worklenz-backend/src/controllers/aws-ses-controller.ts
new file mode 100644
index 00000000..bccfcd83
--- /dev/null
+++ b/worklenz-backend/src/controllers/aws-ses-controller.ts
@@ -0,0 +1,58 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {ISESBouncedMessage} from "../interfaces/aws-bounced-email-response";
+import db from "../config/db";
+import {ISESComplaintMessage} from "../interfaces/aws-complaint-email-response";
+
+export default class AwsSesController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async handleBounceResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const message = JSON.parse(req.body.Message) as ISESBouncedMessage;
+
+ if (message.notificationType === "Bounce" && message.bounce.bounceType === "Permanent") {
+ const bouncedEmails = Array.from(new Set(message.bounce.bouncedRecipients.map(r => r.emailAddress)));
+
+ for (const email of bouncedEmails) {
+ const q = `
+ INSERT INTO bounced_emails (email)
+ VALUES ($1)
+ ON CONFLICT (email) DO UPDATE SET updated_at = CURRENT_TIMESTAMP;
+ `;
+ await db.query(q, [email]);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+
+ @HandleExceptions()
+ public static async handleComplaintResponse(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const message = JSON.parse(req.body.Message) as ISESComplaintMessage;
+
+ if (message.notificationType === "Complaint") {
+ const spamEmails = Array.from(new Set(message.complaint.complainedRecipients.map(r => r.emailAddress)));
+
+ for (const email of spamEmails) {
+ const q = `
+ INSERT INTO spam_emails (email)
+ VALUES ($1)
+ ON CONFLICT (email) DO UPDATE SET updated_at = CURRENT_TIMESTAMP;
+ `;
+ await db.query(q, [email]);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+
+ @HandleExceptions()
+ public static async handleReplies(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ console.log("\n");
+ console.log(JSON.stringify(req.body));
+ console.log("\n");
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+}
diff --git a/worklenz-backend/src/controllers/clients-controller.ts b/worklenz-backend/src/controllers/clients-controller.ts
new file mode 100644
index 00000000..c654ea9f
--- /dev/null
+++ b/worklenz-backend/src/controllers/clients-controller.ts
@@ -0,0 +1,81 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {isValidateEmail} from "../shared/utils";
+import {ServerResponse} from "../models/server-response";
+import {sendNewSubscriberNotification} from "../shared/email-templates";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class ClientsController extends WorklenzControllerBase {
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `INSERT INTO clients (name, team_id) VALUES ($1, $2);`;
+ const result = await db.query(q, [req.body.name, req.user?.team_id || null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS clients
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT COUNT(*) FROM projects WHERE client_id = clients.id) AS projects_count
+ FROM clients
+ WHERE team_id = $1 ${searchQuery}
+ ORDER BY ${sortField} ${sortOrder}
+ LIMIT $2 OFFSET $3) t) AS data
+ FROM clients
+ WHERE team_id = $1 ${searchQuery}) rec;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, size, offset]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data.clients || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name FROM clients WHERE id = $1 AND team_id = $2`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE clients SET name = $3 WHERE id = $1 AND team_id = $2; `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null, req.body.name]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE FROM clients WHERE id = $1 AND team_id = $2;`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async addSubscriber(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {email} = req.body;
+ if (!this.isValidHost(req.hostname))
+ return res.status(200).send(new ServerResponse(false, null, "Invalid hostname"));
+
+ if (!isValidateEmail(email))
+ return res.status(200).send(new ServerResponse(false, null, "Invalid email address"));
+
+ sendNewSubscriberNotification(email);
+
+ return res.status(200).send(new ServerResponse(true, null, "Thank you for subscribing. We'll update you once WorkLenz is live!"));
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/gantt-controller.ts b/worklenz-backend/src/controllers/gantt-controller.ts
new file mode 100644
index 00000000..425cb96b
--- /dev/null
+++ b/worklenz-backend/src/controllers/gantt-controller.ts
@@ -0,0 +1,97 @@
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import { getColor } from "../shared/utils";
+import moment from "moment";
+
+export default class GanttController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getPhaseLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT phase_label
+ FROM projects
+ WHERE id = $1;`;
+ const result = await db.query(q, [req.query.project_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id AS "TaskID",
+ name AS "TaskName",
+ start_date AS "StartDate",
+ end_date AS "EndDate",
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)),
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id AS "TaskID",
+ name AS "TaskName",
+ start_date AS "StartDate",
+ end_date AS "EndDate",
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id))
+ FROM tasks t
+ WHERE t.parent_task_id = tasks.id) rec) AS subtasks
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = $1
+ AND parent_task_id IS NULL
+ ORDER BY roadmap_sort_order, created_at DESC;`;
+ const result = await db.query(q, [req.query.project_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getPhasesByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT name AS label,
+ (SELECT MIN(start_date)
+ FROM tasks
+ WHERE id IN (SELECT task_id FROM task_phase WHERE phase_id = project_phases.id)) as day
+ FROM project_phases
+ WHERE project_id = $1;`;
+ const result = await db.query(q, [req.params.id]);
+ for (const phase of result.rows) {
+ phase.day = new Date(phase.day);
+ }
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getWorkload(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT pm.id AS "TaskID",
+ tmiv.team_member_id,
+ name AS "TaskName",
+ avatar_url,
+ email,
+ TRUE as project_member,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id AS "TaskID",
+ name AS "TaskName",
+ start_date AS "StartDate",
+ end_date AS "EndDate"
+ FROM tasks
+ INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE archived IS FALSE
+ AND project_id = pm.project_id
+ AND ta.team_member_id = tmiv.team_member_id
+ ORDER BY roadmap_sort_order, start_date DESC) rec) AS subtasks
+ FROM project_members pm
+ INNER JOIN team_member_info_view tmiv ON pm.team_member_id = tmiv.team_member_id
+ WHERE project_id = $1
+ ORDER BY tmiv.name;`;
+ const result = await db.query(q, [req.query.project_id]);
+
+ for (const member of result.rows) {
+ member.color_code = getColor(member.TaskName);
+ }
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/home-page-controller.ts b/worklenz-backend/src/controllers/home-page-controller.ts
new file mode 100644
index 00000000..34bfbe6f
--- /dev/null
+++ b/worklenz-backend/src/controllers/home-page-controller.ts
@@ -0,0 +1,422 @@
+import moment from "moment-timezone";
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import momentTime from "moment-timezone";
+
+interface ITask {
+ id: string,
+ name: string,
+ project_id: string,
+ parent_task_id: string | null,
+ is_sub_task: boolean,
+ parent_task_name: string | null,
+ status_id: string,
+ start_date: string | null,
+ end_date: string | null,
+ created_at: string | null,
+ team_id: string,
+ project_name: string,
+ project_color: string | null,
+ status: string,
+ status_color: string | null,
+ is_task: boolean,
+ done: boolean,
+ updated_at: string | null,
+ project_statuses: [{
+ id: string,
+ name: string | null,
+ color_code: string | null,
+ }]
+}
+
+export default class HomePageController extends WorklenzControllerBase {
+
+ private static readonly GROUP_BY_ASSIGNED_TO_ME = "0";
+ private static readonly GROUP_BY_ASSIGN_BY_ME = "1";
+ private static readonly ALL_TAB = "All";
+ private static readonly TODAY_TAB = "Today";
+ private static readonly UPCOMING_TAB = "Upcoming";
+ private static readonly OVERDUE_TAB = "Overdue";
+ private static readonly NO_DUE_DATE_TAB = "NoDueDate";
+
+ private static isValidGroup(groupBy: string) {
+ return groupBy === this.GROUP_BY_ASSIGNED_TO_ME
+ || groupBy === this.GROUP_BY_ASSIGN_BY_ME;
+ }
+
+ private static isValidView(currentView: string) {
+ return currentView === this.ALL_TAB
+ || currentView === this.TODAY_TAB
+ || currentView === this.UPCOMING_TAB
+ || currentView === this.OVERDUE_TAB
+ || currentView === this.NO_DUE_DATE_TAB;
+ }
+
+ @HandleExceptions()
+ public static async createPersonalTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `INSERT INTO personal_todo_list (name, color_code, user_id, index)
+ VALUES ($1, $2, $3, ((SELECT index FROM personal_todo_list ORDER BY index DESC LIMIT 1) + 1))
+ RETURNING id, name`;
+ const result = await db.query(q, [req.body.name, req.body.color_code, req.user?.id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ private static getTasksByGroupClosure(groupBy: string) {
+ switch (groupBy) {
+ case this.GROUP_BY_ASSIGN_BY_ME:
+ return `AND t.id IN (
+ SELECT task_id
+ FROM tasks_assignees
+ WHERE assigned_by = $2 AND team_id = $1)`;
+
+ case this.GROUP_BY_ASSIGNED_TO_ME:
+ default:
+ return `AND t.id IN (
+ SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id FROM team_members WHERE user_id = $2 AND team_id = $1))`;
+ }
+ }
+
+ private static getTasksByTabClosure(text: string) {
+ switch (text) {
+ case this.TODAY_TAB:
+ return `AND t.end_date::DATE = CURRENT_DATE::DATE`;
+ case this.UPCOMING_TAB:
+ return `AND t.end_date::DATE > CURRENT_DATE::DATE`;
+ case this.OVERDUE_TAB:
+ return `AND t.end_date::DATE < CURRENT_DATE::DATE`;
+ case this.NO_DUE_DATE_TAB:
+ return `AND t.end_date IS NULL`;
+ case this.ALL_TAB:
+ default:
+ return "";
+ }
+ }
+
+ private static async getTasksResult(groupByClosure: string, currentTabClosure: string, teamId: string, userId: string) {
+ const q = `
+ SELECT t.id,
+ t.name,
+ t.project_id,
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
+ t.status_id,
+ t.start_date,
+ t.end_date,
+ t.created_at,
+ p.team_id,
+ p.name AS project_name,
+ p.color_code AS project_color,
+ (SELECT id FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ TRUE AS is_task,
+ FALSE AS done,
+ t.updated_at,
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT task_statuses.id AS id,
+ task_statuses.name AS name,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = task_statuses.category_id)
+ FROM task_statuses
+ WHERE task_statuses.project_id = t.project_id) r) AS project_statuses
+ FROM tasks t
+ JOIN projects p ON t.project_id = p.id
+ WHERE t.archived IS FALSE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN (SELECT id
+ FROM sys_task_status_categories
+ WHERE is_done IS FALSE))
+ ${groupByClosure}
+ ORDER BY t.end_date ASC`;
+
+ const result = await db.query(q, [teamId, userId]);
+ return result.rows;
+ }
+
+ private static async getCountsResult(groupByClosure: string, teamId: string, userId: string) {
+ const q = `SELECT COUNT(*) AS total,
+ COUNT(CASE WHEN t.end_date::DATE = CURRENT_DATE::DATE THEN 1 END) AS today,
+ COUNT(CASE WHEN t.end_date::DATE > CURRENT_DATE::DATE THEN 1 END) AS upcoming,
+ COUNT(CASE WHEN t.end_date::DATE < CURRENT_DATE::DATE THEN 1 END) AS overdue,
+ COUNT(CASE WHEN t.end_date::DATE IS NULL THEN 1 END) AS no_due_date
+ FROM tasks t
+ JOIN projects p ON t.project_id = p.id
+ WHERE t.archived IS FALSE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN (SELECT id
+ FROM sys_task_status_categories
+ WHERE is_done IS FALSE))
+ ${groupByClosure}`;
+
+ const result = await db.query(q, [teamId, userId]);
+ const [row] = result.rows;
+ return row;
+ }
+
+ @HandleExceptions()
+ public static async getTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = req.user?.team_id;
+ const userId = req.user?.id;
+ const timeZone = req.query.time_zone as string;
+ const today = new Date();
+
+ const currentGroup = this.isValidGroup(req.query.group_by as string) ? req.query.group_by : this.GROUP_BY_ASSIGNED_TO_ME;
+ const currentTab = this.isValidView(req.query.current_tab as string) ? req.query.current_tab : this.ALL_TAB;
+
+ const groupByClosure = this.getTasksByGroupClosure(currentGroup as string);
+ let currentTabClosure = this.getTasksByTabClosure(currentTab as string);
+
+ const isCalendarView = req.query.is_calendar_view;
+
+ let result = await this.getTasksResult(groupByClosure, currentTabClosure, teamId as string, userId as string);
+
+ const counts = await this.getCountsByGroup(result, timeZone, today);
+
+ if (isCalendarView == "true") {
+ currentTabClosure = `AND t.end_date::DATE = '${req.query.selected_date}'`;
+ result = await this.groupBySingleDate(result, timeZone, req.query.selected_date as string);
+ } else {
+ result = await this.groupByDate(currentTab as string,result, timeZone, today);
+ }
+
+ // const counts = await this.getCountsResult(groupByClosure, teamId as string, userId as string);
+
+ const data = {
+ tasks: result,
+ total: counts.total,
+ today: counts.today,
+ upcoming: counts.upcoming,
+ overdue: counts.overdue,
+ no_due_date: counts.no_due_date,
+ };
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ private static async groupByDate(currentTab: string,tasks: any[], timeZone: string, today: Date) {
+ const formatToday = moment(today).format("YYYY-MM-DD");
+
+ const tasksReturn = [];
+
+ if (currentTab === this.ALL_TAB) {
+ for (const task of tasks) {
+ tasksReturn.push(task);
+ }
+ }
+
+ if (currentTab === this.NO_DUE_DATE_TAB) {
+ for (const task of tasks) {
+ if (!task.end_date) {
+ tasksReturn.push(task);
+ }
+ }
+ }
+
+ if (currentTab === this.TODAY_TAB) {
+ for (const task of tasks) {
+ if (task.end_date) {
+ const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+ if (moment(taskEndDate).isSame(formatToday)) {
+ tasksReturn.push(task);
+ }
+ }
+ }
+ }
+
+ if (currentTab === this.UPCOMING_TAB) {
+ for (const task of tasks) {
+ if (task.end_date) {
+ const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+ if (moment(taskEndDate).isAfter(formatToday)) {
+ tasksReturn.push(task);
+ }
+ }
+ }
+ }
+
+ if (currentTab === this.OVERDUE_TAB) {
+ for (const task of tasks) {
+ if (task.end_date) {
+ const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+ if (moment(taskEndDate).isBefore(formatToday)) {
+ tasksReturn.push(task);
+ }
+ }
+ }
+ }
+
+ return tasksReturn;
+ }
+
+ private static async groupBySingleDate(tasks: any, timeZone: string, selectedDate: string) {
+ const formatSelectedDate = moment(selectedDate).format("YYYY-MM-DD");
+
+ const tasksReturn = [];
+
+ for (const task of tasks) {
+ if (task.end_date) {
+ const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+ if (moment(taskEndDate).isSame(formatSelectedDate)) {
+ tasksReturn.push(task);
+ }
+ }
+ }
+
+ return tasksReturn;
+
+ }
+
+ private static async getCountsByGroup(tasks: any[], timeZone: string, today_: Date) {
+ let no_due_date = 0;
+ let today = 0;
+ let upcoming = 0;
+ let overdue = 0;
+
+ const total = tasks.length;
+
+ const formatToday = moment(today_).format("YYYY-MM-DD");
+
+ for (const task of tasks) {
+ if (!task.end_date) {
+ no_due_date = no_due_date + 1;
+ }
+ if (task.end_date) {
+ const taskEndDate = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+ if (moment(taskEndDate).isSame(formatToday)) {
+ today = today + 1;
+ }
+ if (moment(taskEndDate).isAfter(formatToday)) {
+ upcoming = upcoming + 1;
+ }
+ if (moment(taskEndDate).isBefore(formatToday)) {
+ overdue = overdue + 1;
+ }
+ }
+ }
+
+ return {
+ total,
+ today,
+ upcoming,
+ overdue,
+ no_due_date
+ };
+
+ }
+
+ @HandleExceptions()
+ public static async getPersonalTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const user_id = req.user?.id;
+ const q = `SELECT ptl.id,
+ ptl.name,
+ ptl.created_at,
+ FALSE AS is_task,
+ ptl.done,
+ ptl.updated_at
+ FROM personal_todo_list ptl
+ WHERE ptl.user_id = $1
+ AND done IS FALSE
+ ORDER BY ptl.updated_at DESC`;
+ const results = await db.query(q, [user_id]);
+ return res.status(200).send(new ServerResponse(true, results.rows));
+ }
+
+ @HandleExceptions()
+ public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const team_id = req.user?.team_id;
+ const user_id = req.user?.id;
+
+ const current_view = req.query.view;
+
+ const isFavorites = current_view === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = $2 AND project_id = projects.id)` : "";
+ const isArchived = req.query.filter === "2"
+ ? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = $2 AND project_id = projects.id)`
+ : ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = $2 AND project_id = projects.id)`;
+
+ const q = `SELECT id,
+ name,
+ EXISTS(SELECT user_id
+ FROM favorite_projects
+ WHERE user_id = $2
+ AND project_id = projects.id) AS favorite,
+ EXISTS(SELECT user_id
+ FROM archived_projects
+ WHERE user_id = $2
+ AND project_id = projects.id) AS archived,
+ color_code,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_id = projects.id) AS members_count,
+ (SELECT get_project_members(projects.id)) AS names,
+ (SELECT CASE
+ WHEN ((SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) >
+ updated_at)
+ THEN (SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id)
+ ELSE updated_at END) AS updated_at
+ FROM projects
+ WHERE team_id = $1 ${isArchived} ${isFavorites} AND is_member_of_project(projects.id , $2
+ , $1)
+ ORDER BY updated_at DESC`;
+
+ const result = await db.query(q, [team_id, user_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getProjectsByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const team_id = req.user?.team_id;
+ const user_id = req.user?.id;
+ const q = `
+ SELECT id, name, color_code
+ FROM projects
+ WHERE team_id = $1
+ AND is_member_of_project(projects.id, $2, $1)
+ `;
+ const result = await db.query(q, [team_id, user_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async updatePersonalTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE personal_todo_list
+ SET done = TRUE
+ WHERE id = $1
+ RETURNING id
+ `;
+ await db.query(q, [req.body.id]);
+ return res.status(200).send(new ServerResponse(true, req.body.id));
+ }
+}
diff --git a/worklenz-backend/src/controllers/index-controller.ts b/worklenz-backend/src/controllers/index-controller.ts
new file mode 100644
index 00000000..938e09c9
--- /dev/null
+++ b/worklenz-backend/src/controllers/index-controller.ts
@@ -0,0 +1,102 @@
+import WorklenzControllerBase from "./worklenz-controller-base";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {NextFunction} from "express";
+import FileConstants from "../shared/file-constants";
+import {isInternalServer, isProduction, log_error} from "../shared/utils";
+import db from "../config/db";
+import createHttpError from "http-errors";
+
+export default class IndexController extends WorklenzControllerBase {
+ public static use(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) {
+ try {
+ const url = `https://${req.hostname}${req.url}`;
+ res.locals.release = FileConstants.getRelease();
+ res.locals.user = req.user;
+ res.locals.url = url;
+ res.locals.env = process.env.NODE_ENV;
+ res.locals.isInternalServer = isInternalServer;
+ res.locals.isProduction = isProduction;
+ } catch (error) {
+ console.error(error);
+ }
+ next();
+ }
+
+ public static async index(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const q = `SELECT free_tier_storage, team_member_limit, projects_limit, trial_duration FROM licensing_settings;`;
+ const result = await db.query(q, []);
+ const [settings] = result.rows;
+ res.render("index", {settings});
+ }
+
+ public static pricing(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ res.render("pricing");
+ }
+
+ public static privacyPolicy(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ res.render("privacy-policy");
+ }
+
+ public static termsOfUse(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ res.render("terms-of-use");
+ }
+
+ public static admin(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ res.render("admin");
+ }
+
+ public static auth(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ if (req.isAuthenticated())
+ return res.redirect("/worklenz");
+ return res.render("admin");
+ }
+
+ public static worklenz(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ if (req.isAuthenticated())
+ return res.render("admin");
+
+ if (req.user && !req.user.is_member)
+ return res.redirect("/teams");
+
+ return res.redirect("/auth");
+ }
+
+ public static redirectToLogin(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ res.redirect("/auth/login");
+ }
+
+ public static async signup(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): Promise {
+ try {
+ const teamMemberId = req.query.user as string;
+ const q = `SELECT set_active_team_by_member_id($1);`;
+ await db.query(q, [teamMemberId || null]);
+ } catch (error) {
+ log_error(error, req.query);
+ return next(createHttpError(500));
+ }
+
+ if (req.isAuthenticated())
+ return res.redirect("/worklenz");
+
+ return res.render("admin");
+ }
+
+ public static async login(req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) {
+ // Set active team to invited team
+ try {
+ const teamId = req.query.team as string; // invited team id
+ const userId = req.query.user as string; // invited user's id
+ const q = `SELECT set_active_team($1, $2);`;
+ await db.query(q, [userId || null, teamId || null]);
+ } catch (error) {
+ log_error(error, req.query);
+ return next(createHttpError(500));
+ }
+
+ if (req.isAuthenticated())
+ return res.redirect("/worklenz");
+
+ return res.render("admin");
+ }
+}
diff --git a/worklenz-backend/src/controllers/job-titles-controller.ts b/worklenz-backend/src/controllers/job-titles-controller.ts
new file mode 100644
index 00000000..12e9698f
--- /dev/null
+++ b/worklenz-backend/src/controllers/job-titles-controller.ts
@@ -0,0 +1,62 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class JobTitlesController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {name} = req.body;
+ const q = `INSERT INTO job_titles (name, team_id) VALUES ($1, (SELECT active_team FROM users WHERE id = $2::UUID));`;
+ const result = await db.query(q, [name, req.user?.id || null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS job_titles
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id, name
+ FROM job_titles
+ WHERE team_id = $1 ${searchQuery}
+ ORDER BY ${sortField} ${sortOrder}
+ LIMIT $2 OFFSET $3) t) AS data
+ FROM job_titles
+ WHERE team_id = $1 ${searchQuery}) rec;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, size, offset]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data.job_titles || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name FROM job_titles WHERE id = $1 AND team_id = $2;`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE job_titles SET name = $1 WHERE id = $2 AND team_id = $3;`;
+ const result = await db.query(q, [req.body.name, req.params.id, req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE FROM job_titles WHERE id = $1 AND team_id = $2;`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/labels-controller.ts b/worklenz-backend/src/controllers/labels-controller.ts
new file mode 100644
index 00000000..414c31d3
--- /dev/null
+++ b/worklenz-backend/src/controllers/labels-controller.ts
@@ -0,0 +1,92 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {TASK_PRIORITY_COLOR_ALPHA, WorklenzColorCodes} from "../shared/constants";
+
+export default class LabelsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ WITH lbs AS (SELECT id,
+ name,
+ color_code,
+ (SELECT COUNT(*) FROM task_labels WHERE label_id = team_labels.id) AS usage,
+ EXISTS(SELECT 1
+ FROM task_labels
+ WHERE task_labels.label_id = team_labels.id
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE id = task_labels.task_id
+ AND project_id = $2)) AS used
+ FROM team_labels
+ WHERE team_id = $1
+ ORDER BY name)
+ SELECT id, name, color_code, usage
+ FROM lbs
+ ORDER BY used DESC;
+ `;
+ const result = await db.query(q, [req.user?.team_id, req.query.project || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getByTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT (SELECT name FROM team_labels WHERE id = task_labels.label_id),
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = $1;
+ `;
+ const result = await db.query(q, [req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, name, color_code
+ FROM team_labels
+ WHERE team_id = $2
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE project_id = $1
+ AND EXISTS(SELECT 1 FROM task_labels WHERE task_id = tasks.id AND label_id = team_labels.id))
+ ORDER BY name;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+
+ for (const label of result.rows) {
+ label.color_code = label.color_code + TASK_PRIORITY_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async updateColor(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE team_labels
+ SET color_code = $3
+ WHERE id = $1
+ AND team_id = $2;`;
+
+ if (!WorklenzColorCodes.includes(req.body.color))
+ return res.status(400).send(new ServerResponse(false, null));
+
+ const result = await db.query(q, [req.params.id, req.user?.team_id, req.body.color]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE
+ FROM team_labels
+ WHERE id = $1
+ AND team_id = $2;`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/logs-controller.ts b/worklenz-backend/src/controllers/logs-controller.ts
new file mode 100644
index 00000000..c9d36cde
--- /dev/null
+++ b/worklenz-backend/src/controllers/logs-controller.ts
@@ -0,0 +1,26 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class LogsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getActivityLog(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT description, (SELECT name FROM projects WHERE projects.id = project_logs.project_id) AS project_name, created_at
+ FROM project_logs
+ WHERE team_id = $1
+ AND (CASE
+ WHEN (is_owner($2, $1) OR
+ is_admin($2, $1)) THEN TRUE
+ ELSE is_member_of_project(project_id, $2, $1) END)
+ ORDER BY created_at DESC
+ LIMIT 5;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/notification-controller.ts b/worklenz-backend/src/controllers/notification-controller.ts
new file mode 100644
index 00000000..9e88f346
--- /dev/null
+++ b/worklenz-backend/src/controllers/notification-controller.ts
@@ -0,0 +1,151 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getColor} from "../shared/utils";
+
+export default class NotificationController extends WorklenzControllerBase {
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT un.id,
+ un.message,
+ un.created_at,
+ un.read,
+ (SELECT name FROM teams WHERE id = un.team_id) AS team,
+ (SELECT name FROM projects WHERE id = t.project_id) AS project,
+ (SELECT color_code FROM projects WHERE id = t.project_id) AS color,
+ t.project_id,
+ t.id AS task_id,
+ un.team_id
+ FROM user_notifications un
+ LEFT JOIN tasks t ON un.task_id = t.id
+ WHERE user_id = $1
+ AND read = $2
+ ORDER BY created_at DESC
+ LIMIT 100;
+ `;
+
+ const result = await db.query(q, [req.user?.id, req.query.filter === "Read"]);
+
+ for (const item of result.rows) {
+ item.team_color = getColor(item.team_name);
+ item.url = item.project_id ? `/worklenz/projects/${item.project_id}` : null;
+ item.params = {task: item.task_id, tab: "tasks-list"};
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT email_notifications_enabled, popup_notifications_enabled, show_unread_items_count, daily_digest_enabled
+ FROM notification_settings
+ WHERE user_id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.user?.id, req.user?.team_id]);
+ const [data] = result.rows;
+
+ const settings = {
+ email_notifications_enabled: !!data?.email_notifications_enabled,
+ popup_notifications_enabled: !!data?.popup_notifications_enabled,
+ show_unread_items_count: !!data?.show_unread_items_count,
+ daily_digest_enabled: !!data?.daily_digest_enabled
+ };
+
+ return res.status(200).send(new ServerResponse(true, settings));
+ }
+
+ @HandleExceptions()
+ public static async getUnreadCount(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT COALESCE(COUNT(*)::INTEGER, 0) AS notifications_count,
+ (SELECT COALESCE(COUNT(*)::INTEGER, 0) FROM email_invitations WHERE email = (SELECT email FROM users WHERE id = $1)) AS invitations_count
+ FROM user_notifications
+ WHERE user_id = $1
+ AND read = false
+ `;
+
+ const result = await db.query(q, [req.user?.id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data.notifications_count + data.invitations_count));
+ }
+
+ @HandleExceptions()
+ public static async updateSettings(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE notification_settings
+ SET email_notifications_enabled = $3,
+ popup_notifications_enabled = $4,
+ show_unread_items_count = $5,
+ daily_digest_enabled = $6
+ WHERE user_id = $1
+ AND team_id = $2
+ RETURNING email_notifications_enabled,
+ popup_notifications_enabled,
+ show_unread_items_count,
+ daily_digest_enabled;
+ `;
+
+ const result = await db.query(q, [
+ req.user?.id,
+ req.user?.team_id,
+ !!req.body.email_notifications_enabled,
+ !!req.body.popup_notifications_enabled,
+ !!req.body.show_unread_items_count,
+ !!req.body.daily_digest_enabled
+ ]);
+ const [data] = result.rows;
+
+ const settings = {
+ email_notifications_enabled: !!data?.email_notifications_enabled,
+ popup_notifications_enabled: !!data?.popup_notifications_enabled,
+ show_unread_items_count: !!data?.show_unread_items_count,
+ daily_digest_enabled: !!data?.daily_digest_enabled
+ };
+
+ return res.status(200).send(new ServerResponse(true, settings));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE user_notifications
+ SET read = TRUE
+ WHERE id = $1
+ AND user_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async delete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ DELETE
+ FROM user_notifications
+ WHERE id = $1
+ AND user_id = $2
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async readAll(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE user_notifications
+ SET read = TRUE
+ WHERE user_id = $1
+ AND read IS FALSE;
+ `;
+ const result = await db.query(q, [req.user?.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/overview-controller.ts b/worklenz-backend/src/controllers/overview-controller.ts
new file mode 100644
index 00000000..465bc62a
--- /dev/null
+++ b/worklenz-backend/src/controllers/overview-controller.ts
@@ -0,0 +1,50 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class OverviewController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id,
+ name,
+ color_code,
+ notes,
+ (SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT team_member_id AS id,
+ (SELECT task_id
+ FROM tasks_assignees
+ WHERE EXISTS(SELECT id FROM tasks WHERE project_id = $1)
+ AND project_member_id = id) AS task_count,
+ (SELECT name
+ FROM users
+ WHERE id =
+ (SELECT user_id
+ FROM team_members
+ WHERE team_member_id = project_members.team_member_id)),
+ (SELECT name
+ FROM job_titles
+ WHERE id = (SELECT job_title_id
+ FROM team_members
+ WHERE id = project_members.team_member_id)) AS job_title
+ FROM project_members
+ WHERE project_id = projects.id
+ ORDER BY name ASC) rec) AS members,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id, name, done FROM tasks WHERE project_id = projects.id ORDER BY name ASC) rec) AS tasks
+ FROM projects
+ WHERE id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+}
diff --git a/worklenz-backend/src/controllers/personal-overview-controller.ts b/worklenz-backend/src/controllers/personal-overview-controller.ts
new file mode 100644
index 00000000..cbfa8ded
--- /dev/null
+++ b/worklenz-backend/src/controllers/personal-overview-controller.ts
@@ -0,0 +1,72 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class PersonalOverviewController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getTasksDueToday(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id,
+ name,
+ (SELECT name FROM projects WHERE project_id = projects.id) AS project_name,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT task_priorities.name FROM task_priorities WHERE id = t.priority_id) AS priority,
+ start_date,
+ end_date
+ FROM tasks t
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE t.archived IS FALSE AND t.end_date::DATE = NOW()::DATE
+ AND is_member_of_project(t.project_id, $2, $1)
+ ORDER BY end_date DESC
+ LIMIT 5;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getTasksRemaining(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id,
+ name,
+ (SELECT name FROM projects WHERE project_id = projects.id) AS project_name,
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status,
+ (SELECT task_priorities.name FROM task_priorities WHERE id = t.priority_id) AS priority,
+ start_date,
+ end_date
+ FROM tasks t
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE t.archived IS FALSE AND t.end_date::DATE > NOW()::DATE
+ AND is_member_of_project(t.project_id, $2, $1)
+ ORDER BY end_date DESC
+ LIMIT 5;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getTaskOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id,
+ name,
+ color_code,
+ (SELECT MIN(start_date) FROM tasks WHERE archived IS FALSE AND project_id = projects.id) AS min_date,
+ (SELECT MAX(end_date) FROM tasks WHERE archived IS FALSE AND project_id = projects.id) AS max_date
+ FROM projects
+ WHERE team_id = $1
+ AND (CASE
+ WHEN (is_owner($2, $1) OR
+ is_admin($2, $1)) THEN TRUE
+ ELSE is_member_of_project(projects.id, $2,
+ $1) END)
+ ORDER BY NAME;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/profile-settings-controller.ts b/worklenz-backend/src/controllers/profile-settings-controller.ts
new file mode 100644
index 00000000..432ff44a
--- /dev/null
+++ b/worklenz-backend/src/controllers/profile-settings-controller.ts
@@ -0,0 +1,66 @@
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {IPassportSession} from "../interfaces/passport-session";
+
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import {ServerResponse} from "../models/server-response";
+import {NotificationsService} from "../services/notifications/notifications.service";
+import {slugify} from "../shared/utils";
+import {generateProjectKey} from "../utils/generate-project-key";
+import WorklenzControllerBase from "./worklenz-controller-base";
+
+export default class ProfileSettingsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async setup(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT complete_account_setup($1, $2, $3) AS account;`;
+ req.body.key = generateProjectKey(req.body.project_name, []) || null;
+ const result = await db.query(q, [req.user?.id, req.user?.team_id, JSON.stringify(req.body)]);
+ const [data] = result.rows;
+
+ if (!data)
+ return res.status(200).send(new ServerResponse(false, null, "Account setup failed! Please try again"));
+
+ const newMembers = data.account.members || [];
+
+ NotificationsService.sendTeamMembersInvitations(newMembers, req.user as IPassportSession);
+
+ return res.status(200).send(new ServerResponse(true, data.account));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT name, email
+ FROM users
+ WHERE id = $1;`;
+ const result = await db.query(q, [req.user?.id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE users
+ SET name = $2,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = $1
+ RETURNING id, name, email;`;
+ const result = await db.query(q, [req.user?.id, req.body.name]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update_team_name(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT update_team_name($1);`;
+ const body = {
+ id: req.params.id,
+ name: req.body.name,
+ key: slugify(req.body.name),
+ user_id: req.user?.id
+ };
+ await db.query(q, [JSON.stringify(body)]);
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-categories-controller.ts b/worklenz-backend/src/controllers/project-categories-controller.ts
new file mode 100644
index 00000000..220355a5
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-categories-controller.ts
@@ -0,0 +1,97 @@
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import { getColor } from "../shared/utils";
+import { WorklenzColorCodes } from "../shared/constants";
+
+export default class ProjectCategoriesController extends WorklenzControllerBase {
+
+ private static flatString(text: string) {
+ return (text || "").split(",").map(s => `'${s}'`).join(",");
+ }
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ INSERT INTO project_categories (name, team_id, created_by, color_code)
+ VALUES ($1, $2, $3, $4)
+ RETURNING id, name, color_code;
+ `;
+ const name = req.body.name.trim();
+ const result = await db.query(q, [name, req.user?.team_id, req.user?.id, name ? getColor(name) : null]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, name, color_code, (SELECT COUNT(*) FROM projects WHERE category_id = project_categories.id) AS usage
+ FROM project_categories
+ WHERE team_id = $1;
+ `;
+ const result = await db.query(q, [req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, name, color_code, (SELECT COUNT(*) FROM projects WHERE category_id = project_categories.id) AS usage
+ FROM project_categories
+ WHERE team_id = $1;`;
+ const result = await db.query(q, [req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ private static async getTeamsByOrg(teamId: string) {
+ const q = `SELECT id FROM teams WHERE in_organization(id, $1)`;
+ const result = await db.query(q, [teamId]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getByMultipleTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const teams = await this.getTeamsByOrg(req.user?.team_id as string);
+ const teamIds = teams.map(team => team.id).join(",");
+
+ const q = `SELECT id, name, color_code FROM project_categories WHERE team_id IN (${this.flatString(teamIds)});`;
+
+ const result = await db.query(q);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE project_categories
+ SET color_code = $2
+ WHERE id = $1
+ AND team_id = $3;
+ `;
+
+ if (!WorklenzColorCodes.includes(req.body.color))
+ return res.status(400).send(new ServerResponse(false, null));
+
+ const result = await db.query(q, [req.params.id, req.body.color, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ DELETE
+ FROM project_categories
+ WHERE id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-comments-controller.ts b/worklenz-backend/src/controllers/project-comments-controller.ts
new file mode 100644
index 00000000..26777221
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-comments-controller.ts
@@ -0,0 +1,241 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getColor, slugify} from "../shared/utils";
+import { HTML_TAG_REGEXP } from "../shared/constants";
+import { IProjectCommentEmailNotification } from "../interfaces/comment-email-notification";
+import { sendProjectComment } from "../shared/email-notifications";
+import { NotificationsService } from "../services/notifications/notifications.service";
+import { IO } from "../shared/io";
+import { SocketEvents } from "../socket.io/events";
+
+interface IMailConfig {
+ message: string;
+ receiverEmail: string;
+ receiverName: string;
+ content: string;
+ teamName: string;
+ projectName: string;
+}
+
+interface IMention {
+ id: string;
+ name: string;
+}
+
+export default class ProjectCommentsController extends WorklenzControllerBase {
+
+ private static replaceContent(messageContent: string, mentions: { id: string; name: string }[]) {
+ const mentionNames = mentions.map(mention => mention.name);
+
+ const replacedContent = mentionNames.reduce(
+ (content, mentionName, index) => {
+ const regex = new RegExp(`@${mentionName}`, "g");
+ return content.replace(regex, `{${index}}`);
+ },
+ messageContent
+ );
+
+ return replacedContent;
+ }
+
+ private static async sendMail(config: IMailConfig) {
+ const subject = config.message.replace(HTML_TAG_REGEXP, "");
+
+ const data: IProjectCommentEmailNotification = {
+ greeting: `Hi ${config.receiverName}`,
+ summary: subject,
+ team: config.teamName,
+ project_name: config.projectName,
+ comment: config.content
+ };
+
+ await sendProjectComment(config.receiverEmail, data);
+ }
+
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const userId = req.user?.id;
+ const mentions: IMention[] = req.body.mentions;
+ const projectId = req.body.project_id;
+ const teamId = req.user?.team_id;
+
+ let commentContent = req.body.content;
+ if (mentions.length > 0) {
+ commentContent = await this.replaceContent(commentContent, mentions);
+ }
+
+ const body = {
+ project_id : projectId,
+ created_by: userId,
+ content: commentContent,
+ mentions,
+ team_id: teamId
+ };
+
+ const q = `SELECT create_project_comment($1) AS comment`;
+ const result = await db.query(q, [JSON.stringify(body)]);
+ const [data] = result.rows;
+
+ const projectMembers = await this.getMembersList(projectId);
+
+ const commentMessage = `${req.user?.name} added a comment on ${data.comment.project_name} (${data.comment.team_name})`;
+
+ for (const member of projectMembers || []) {
+ if (member.id && member.id === req.user?.id) continue;
+ NotificationsService.createNotification({
+ userId: member.id,
+ teamId: req.user?.team_id as string,
+ socketId: member.socket_id,
+ message: commentMessage,
+ taskId: null,
+ projectId
+ });
+ if (member.id !== req.user?.id && member.socket_id) {
+ IO.emit(SocketEvents.NEW_PROJECT_COMMENT_RECEIVED, member.socket_id, true);
+ }
+ }
+
+ const mentionMessage = `${req.user?.name} has mentioned you in a comment on ${data.comment.project_name} (${data.comment.team_name})`;
+ const rdMentions = [...new Set(req.body.mentions || [])] as IMention[]; // remove duplicates
+
+ for (const mention of rdMentions) {
+ if (mention) {
+ const member = await this.getUserDataByUserId(mention.id, projectId, teamId as string);
+ NotificationsService.sendNotification({
+ team: data.comment.team_name,
+ receiver_socket_id: member.socket_id,
+ message: mentionMessage,
+ task_id: "",
+ project_id: projectId,
+ project: data.comment.project_name,
+ project_color: member.project_color,
+ team_id: req.user?.team_id as string
+ });
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ private static async getUserDataByUserId(informedBy: string, projectId: string, team_id: string) {
+ const q = `
+ SELECT id,
+ name,
+ email,
+ socket_id,
+ (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE notification_settings.team_id = $3
+ AND notification_settings.user_id = $1),
+ (SELECT color_code FROM projects WHERE id = $2) AS project_color
+ FROM users
+ WHERE id = $1;
+ `;
+ const result = await db.query(q, [informedBy, projectId, team_id]);
+ const [data] = result.rows;
+ return data;
+ }
+
+ private static async getMembersList(projectId: string) {
+ const q = `
+ SELECT
+ tm.user_id AS id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id) AS email,
+ (SELECT socket_id FROM users WHERE users.id = tm.user_id) AS socket_id,
+ (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE team_id = tm.team_id
+ AND notification_settings.user_id = tm.user_id) AS email_notifications_enabled
+ FROM project_members
+ INNER JOIN team_members tm ON project_members.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = $1 AND tm.user_id IS NOT NULL
+ ORDER BY name
+ `;
+ const result = await db.query(q, [projectId]);
+ const members = result.rows;
+ return members;
+ }
+
+ @HandleExceptions()
+ public static async getMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const members = await this.getMembersList(req.params.id as string);
+ return res.status(200).send(new ServerResponse(true, members || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const limit = req.query.isLimit;
+
+ const q = `
+ SELECT
+ pc.id,
+ pc.content AS content,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT u.name AS user_name,
+ u.email AS user_email
+ FROM project_comment_mentions pcm
+ LEFT JOIN users u ON pcm.informed_by = u.id
+ WHERE pcm.comment_id = pc.id) rec) AS mentions,
+ (SELECT id FROM users WHERE id = pc.created_by) AS user_id,
+ (SELECT name FROM users WHERE id = pc.created_by) AS created_by,
+ (SELECT avatar_url FROM users WHERE id = pc.created_by),
+ pc.created_at,
+ pc.updated_at
+ FROM project_comments pc
+ WHERE pc.project_id = $1 ORDER BY pc.updated_at DESC
+ `;
+ const result = await db.query(q, [req.params.id]);
+
+ const data = result.rows;
+
+ for (const comment of data) {
+ const {mentions} = comment;
+ if (mentions.length > 0) {
+ const placeHolders = comment.content.match(/{\d+}/g);
+ if (placeHolders) {
+ comment.content = await comment.content.replace(/\n/g, "");
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < comment.mentions.length) {
+ comment.content = comment.content.replace(placeHolder, `@${comment.mentions[index].user_name}`);
+ }
+ });
+ }
+ }
+ const color_code = getColor(comment.created_by);
+ comment.color_code = color_code;
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getCountByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT COUNT(*) AS total FROM project_comments WHERE project_id = $1`;
+ const result = await db.query(q, [req.params.id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, parseInt(data.total)));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE FROM project_comments WHERE id = $1 RETURNING id`;
+ const result = await db.query(q, [req.params.id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-folders-controller.ts b/worklenz-backend/src/controllers/project-folders-controller.ts
new file mode 100644
index 00000000..81010581
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-folders-controller.ts
@@ -0,0 +1,103 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {slugify} from "../shared/utils";
+import {IProjectFolder} from "../interfaces/project-folder";
+
+export default class ProjectFoldersController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ INSERT INTO project_folders (name, key, created_by, team_id, color_code)
+ VALUES ($1, $2, $3, $4, $5)
+ RETURNING id, name, key, color_code;
+ `;
+
+ const name = req.body.name?.trim() || null;
+ const key = slugify(name);
+ const createdBy = req.user?.id ?? null;
+ const teamId = req.user?.team_id ?? null;
+ const colorCode = req.body.color_code?.trim() || "#70a6f3";
+
+ const result = await db.query(q, [name, key, createdBy, teamId, colorCode]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const parentFolderId = (req.query.parent as string)?.trim() || null;
+
+ const q = [
+ `SELECT id,
+ name,
+ key,
+ color_code,
+ created_at,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE user_id = project_folders.created_by
+ AND team_member_info_view.team_id = project_folders.team_id) AS created_by
+ FROM project_folders
+ WHERE team_id = $1
+ `,
+ parentFolderId ? `AND parent_folder_id = $2` : "",
+ `ORDER BY name;`
+ ].join(" ");
+ const params = parentFolderId ? [req.user?.team_id, parentFolderId] : [req.user?.team_id];
+
+ const result = await db.query(q, params);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, key, name, color_code
+ FROM project_folders
+ WHERE key = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE project_folders
+ SET name = $2,
+ key = $3,
+ color_code = COALESCE($5, color_code),
+ updated_at = CURRENT_TIMESTAMP
+ WHERE id = $1
+ AND team_id = $4
+ RETURNING id, name, key;
+ `;
+
+ const name = req.body.name?.trim() || null;
+ const key = slugify(name);
+ const colorCode = req.body.color_code?.trim() || null;
+
+ const result = await db.query(q, [req.params.id, name, key, req.user?.team_id, colorCode]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ DELETE
+ FROM project_folders
+ WHERE id = $1
+ AND team_id = $2;
+ `;
+ await db.query(q, [req.params.id, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, null));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-healths-controller.ts b/worklenz-backend/src/controllers/project-healths-controller.ts
new file mode 100644
index 00000000..b86912af
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-healths-controller.ts
@@ -0,0 +1,16 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class ProjectHealthController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, color_code, is_default FROM sys_project_healths ORDER BY sort_order;`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+ }
\ No newline at end of file
diff --git a/worklenz-backend/src/controllers/project-insights-controller.ts b/worklenz-backend/src/controllers/project-insights-controller.ts
new file mode 100644
index 00000000..aeb3707d
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-insights-controller.ts
@@ -0,0 +1,343 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {PriorityColorCodes, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {formatDuration, getColor} from "../shared/utils";
+import moment from "moment";
+
+export default class ProjectInsightsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `SELECT get_project_overview_data($1, $2) AS overview;`;
+ const result = await db.query(q, [req.params.id, archived === "true"]);
+ const [data] = result.rows;
+
+ const {total_minutes_sum, time_spent_sum} = data.overview;
+
+ const totalMinutes = moment.duration(total_minutes_sum, "minutes");
+ const totalSeconds = moment.duration(time_spent_sum, "seconds");
+
+ data.overview.total_estimated_hours_string = formatDuration(totalMinutes);
+ data.overview.total_logged_hours_string = formatDuration(totalSeconds);
+
+ data.overview.overlogged_hours = formatDuration(totalMinutes.subtract(totalSeconds));
+
+ return res.status(200).send(new ServerResponse(true, data.overview));
+ }
+
+ public static async getMemberInsightsByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `SELECT get_project_member_insights($1, $2) AS overview;`;
+ const result = await db.query(q, [req.params.id, archived === "true"]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data.overview));
+ }
+
+ @HandleExceptions()
+ public static async getLastUpdatedtasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `SELECT get_last_updated_tasks_by_project($1, $2, $3, $4) AS last_updated;`;
+ const result = await db.query(q, [req.params.id, 10, 0, archived]);
+ const [data] = result.rows;
+
+ for (const task of data.last_updated) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, data.last_updated));
+ }
+
+
+ @HandleExceptions()
+ public static async getProjectLogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT description, created_at
+ FROM project_logs
+ WHERE project_id = $1
+ ORDER BY created_at DESC
+ LIMIT $2 OFFSET $3;`;
+ const result = await db.query(q, [req.params.id, 10, 0]);
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getStatusOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `
+ SELECT task_statuses.id,
+ task_statuses.name,
+ stsc.color_code
+ FROM task_statuses
+ INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
+ WHERE project_id = $1
+ AND team_id = $2
+ ORDER BY task_statuses.sort_order;`;
+ const status = await db.query(q, [req.params.id, req.user?.team_id]);
+ const statusCounts = [];
+
+ for (const element of status.rows) {
+ const q = `SELECT COUNT(*)
+ FROM tasks
+ WHERE status_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END;`;
+ const count = await db.query(q, [element.id, archived === "true"]);
+ const [data] = count.rows;
+ statusCounts.push({name: element.name, color: element.color_code, y: parseInt(data.count)});
+ element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, statusCounts || []));
+ }
+
+ @HandleExceptions()
+ public static async getPriorityOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `SELECT id, name, value
+ FROM task_priorities
+ ORDER BY value;`;
+ const result = await db.query(q, []);
+ for (const item of result.rows)
+ item.color_code = PriorityColorCodes[item.value] || PriorityColorCodes["0"];
+
+ const statusCounts = [];
+
+ for (const element of result.rows) {
+ const q = `SELECT COUNT(*)
+ FROM tasks
+ WHERE priority_id = $1
+ AND CASE
+ WHEN ($3 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND project_id = $2;`;
+ const count = await db.query(q, [element.id, req.params.id, archived === "true"]);
+ const [data] = count.rows;
+ statusCounts.push({name: element.name, color: element.color_code, data: [parseInt(data.count)]});
+ }
+
+ return res.status(200).send(new ServerResponse(true, statusCounts || []));
+ }
+
+ @HandleExceptions()
+ public static async getOverdueTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `
+ SELECT id,
+ name,
+ status_id AS status,
+ end_date,
+ priority_id AS priority,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ updated_at,
+ NOW()::DATE - end_date::DATE AS days_overdue,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
+ FROM tasks
+ WHERE project_id = $1
+ AND end_date::DATE < NOW()::DATE
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = $1
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS FALSE));
+ `;
+ const result = await db.query(q, [req.params.id, archived]);
+
+ for (const element of result.rows) {
+ element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getTasksFinishedEarly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `
+ SELECT id,
+ name,
+ status_id AS status,
+ end_date,
+ priority_id AS priority,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ updated_at,
+ completed_at,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
+ FROM tasks
+ WHERE project_id = $1
+ AND completed_at::DATE < end_date::DATE
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = $1
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS TRUE));
+ `;
+ const result = await db.query(q, [req.params.id, archived]);
+
+ for (const element of result.rows) {
+ element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getTasksFinishedLate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `
+ SELECT id,
+ name,
+ status_id AS status,
+ end_date,
+ priority_id AS priority,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ updated_at,
+ completed_at,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color
+ FROM tasks
+ WHERE project_id = $1
+ AND completed_at::DATE > end_date::DATE
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = $1
+ AND category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.is_done IS TRUE));
+ `;
+ const result = await db.query(q, [req.params.id, archived]);
+
+ for (const element of result.rows) {
+ element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getTasksByProjectMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {member_id, project_id, archived} = req.body;
+ const q = `SELECT get_tasks_by_project_member($1, $2, $3)`;
+ const result = await db.query(q, [project_id || null, member_id || null, archived]);
+ const [data] = result.rows;
+
+ for (const element of data.get_tasks_by_project_member) {
+ element.status_color = element.status_color + TASK_STATUS_COLOR_ALPHA;
+ element.total_minutes = formatDuration(moment.duration(~~(element.total_minutes), "minutes"));
+ element.overlogged_time = formatDuration(moment.duration(element.overlogged_time, "seconds"));
+ }
+
+ return res.status(200).send(new ServerResponse(true, data.get_tasks_by_project_member || []));
+ }
+
+ @HandleExceptions()
+ public static async getProjectDeadlineStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ const q = `SELECT get_project_deadline_tasks($1, $2);`;
+ const result = await db.query(q, [req.params.id || null, archived === "true"]);
+ const [data] = result.rows;
+
+ for (const task of data.get_project_deadline_tasks.tasks) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ const logged_hours = data.get_project_deadline_tasks.deadline_logged_hours || 0; // in seconds
+ data.get_project_deadline_tasks.deadline_logged_hours_string = formatDuration(moment.duration(logged_hours, "seconds"));
+
+ return res.status(200).send(new ServerResponse(true, data.get_project_deadline_tasks || {}));
+ }
+
+
+ @HandleExceptions()
+ public static async getOverloggedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+
+ /**
+ SELECT id,
+ name,
+ status_id AS status,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ end_date,
+ priority_id AS priority,
+ updated_at,
+ ((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) - total_minutes) AS overlogged_time,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
+ (SELECT get_task_assignees(tasks.id)) AS assignees
+ FROM tasks
+ WHERE project_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND total_minutes < (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id);
+ */
+ const q = `
+ WITH work_log AS (SELECT task_id, SUM(time_spent) AS total_time_spent
+ FROM task_work_log
+ GROUP BY task_id)
+ SELECT id,
+ name,
+ status_id AS status,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ end_date,
+ priority_id AS priority,
+ updated_at,
+ (work_log.total_time_spent - (total_minutes * 60)) AS overlogged_time,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
+ (SELECT get_task_assignees(tasks.id)) AS assignees
+ FROM tasks
+ JOIN work_log ON work_log.task_id = tasks.id
+ WHERE project_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND total_minutes < work_log.total_time_spent;
+ `;
+
+ const result = await db.query(q, [req.params.id || null, archived]);
+
+ for (const task of result.rows) {
+ task.overlogged_time_string = formatDuration(moment.duration(task.overlogged_time, "seconds"));
+ task.assignees.map((a: any) => a.color_code = getColor(a.name));
+ task.names = this.createTagList(task.assignees);
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-managers-controller.ts b/worklenz-backend/src/controllers/project-managers-controller.ts
new file mode 100644
index 00000000..a46fae85
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-managers-controller.ts
@@ -0,0 +1,45 @@
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+
+
+export default class ProjectManagersController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getByOrg(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ // const q = `SELECT DISTINCT (SELECT user_id from team_member_info_view tmv WHERE tmv.team_member_id = pm.team_member_id),
+ // team_member_id,
+ // (SELECT name from team_member_info_view tmv WHERE tmv.team_member_id = pm.team_member_id)
+ // FROM project_members pm
+ // WHERE project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
+ // AND pm.project_id IN (SELECT id FROM projects WHERE team_id IN (SELECT id FROM teams WHERE in_organization(id, $1)));`;
+ // const q = `SELECT DISTINCT tmv.user_id,
+ // tmv.name,
+ // pm.team_member_id
+ // FROM team_member_info_view tmv
+ // INNER JOIN project_members pm ON tmv.team_member_id = pm.team_member_id
+ // INNER JOIN projects p ON pm.project_id = p.id
+ // WHERE pm.project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
+ // AND p.team_id IN (SELECT id FROM teams WHERE in_organization(id, $1))`;
+ const q = `SELECT DISTINCT ON (tm.user_id)
+ tm.user_id AS id,
+ u.name,
+ pm.team_member_id
+ FROM
+ projects p
+ JOIN project_members pm ON p.id = pm.project_id
+ JOIN teams t ON p.team_id = t.id
+ JOIN team_members tm ON pm.team_member_id = tm.id
+ JOIN team_member_info_view tmi ON tm.id = tmi.team_member_id
+ JOIN users u ON tm.user_id = u.id
+ WHERE
+ t.id IN (SELECT id FROM teams WHERE in_organization(id, $1))
+ AND pm.project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')
+ GROUP BY
+ tm.user_id, u.name, pm.team_member_id`;
+ const result = await db.query(q, [_req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-members-controller.ts b/worklenz-backend/src/controllers/project-members-controller.ts
new file mode 100644
index 00000000..b0fb6e17
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-members-controller.ts
@@ -0,0 +1,150 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getColor} from "../shared/utils";
+import TeamMembersController from "./team-members-controller";
+import {NotificationsService} from "../services/notifications/notifications.service";
+
+export default class ProjectMembersController extends WorklenzControllerBase {
+
+ public static async checkIfUserAlreadyExists(owner_id: string, email: string) {
+ if (!owner_id) throw new Error("Owner not found.");
+
+ const q = `SELECT EXISTS(SELECT tmi.team_member_id
+ FROM team_member_info_view AS tmi
+ JOIN teams AS t ON tmi.team_id = t.id
+ WHERE tmi.email = $1::TEXT
+ AND t.user_id = $2::UUID);`;
+ const result = await db.query(q, [email, owner_id]);
+
+ const [data] = result.rows;
+ return data.exists;
+ }
+
+ public static async createOrInviteMembers(body: any) {
+ if (!body) return;
+
+ const q = `SELECT create_project_member($1) AS res;`;
+
+ const result = await db.query(q, [JSON.stringify(body)]);
+ const [data] = result.rows;
+
+ const response = data.res;
+
+ if (response?.notification && response?.member_user_id) {
+ NotificationsService.sendNotification({
+ receiver_socket_id: response.socket_id,
+ project: response.project,
+ message: response.notification,
+ project_color: response.project_color,
+ project_id: response.project_id,
+ team: response.team,
+ team_id: body.team_id
+ });
+ }
+ return data;
+ }
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ req.body.user_id = req.user?.id;
+ req.body.team_id = req.user?.team_id;
+ req.body.access_level = req.body.access_level ? req.body.access_level : "MEMBER";
+ const data = await this.createOrInviteMembers(req.body);
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "ERROR_EMAIL_INVITATION_EXISTS": "Member already have a pending invitation that has not been accepted."
+ }
+ })
+ public static async createByEmail(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ req.body.user_id = req.user?.id;
+ req.body.team_id = req.user?.team_id;
+
+ if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing."));
+
+ // Adding as a team member
+ const teamMemberReq: { team_id?: string; emails: string[], project_id?: string; } = {
+ team_id: req.user?.team_id,
+ emails: [req.body.email]
+ };
+
+ if (req.body.project_id)
+ teamMemberReq.project_id = req.body.project_id;
+
+ const [member] = await TeamMembersController.createOrInviteMembers(teamMemberReq, req.user);
+
+ if (!member)
+ return res.status(200).send(new ServerResponse(true, null, "Failed to add the member to the project. Please try again."));
+
+ // Adding to the project
+ const projectMemberReq = {
+ team_member_id: member.team_member_id,
+ team_id: req.user?.team_id,
+ project_id: req.body.project_id,
+ user_id: req.user?.id,
+ access_level: req.body.access_level ? req.body.access_level : "MEMBER"
+ };
+ const data = await this.createOrInviteMembers(projectMemberReq);
+ return res.status(200).send(new ServerResponse(true, data.member));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT project_members.id,
+ tm.id AS team_member_id,
+ (SELECT email FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT name FROM team_member_info_view WHERE team_member_id = project_members.team_member_id) AS name,
+ u.avatar_url,
+ jt.name AS job_title
+ FROM project_members
+ INNER JOIN team_members tm ON project_members.team_member_id = tm.id
+ LEFT JOIN job_titles jt ON tm.job_title_id = jt.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = $1
+ ORDER BY project_members.created_at DESC;
+ `;
+ const result = await db.query(q, [req.params.id]);
+
+ result.rows.forEach((a: any) => a.color_code = getColor(a.name));
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ public static async checkIfMemberExists(projectId: string, teamMemberId: string) {
+ const q = `SELECT EXISTS(SELECT id FROM project_members WHERE project_id = $1::UUID AND team_member_id = $2::UUID)`;
+ const result = await db.query(q, [projectId, teamMemberId]);
+ const [data] = result.rows;
+ return data.exists;
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT remove_project_member($1, $2, $3) AS res;`;
+ const result = await db.query(q, [req.params.id, req.user?.id, req.user?.team_id]);
+ const [data] = result.rows;
+
+ const response = data.res;
+
+ if (response?.notification && response?.member_user_id) {
+ NotificationsService.sendNotification({
+ receiver_socket_id: response.socket_id,
+ project: response.project,
+ message: response.notification,
+ project_color: response.project_color,
+ project_id: response.project_id,
+ team: response.team,
+ team_id: req.user?.team_id as string
+ });
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-contoller-v2-base.ts b/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-contoller-v2-base.ts
new file mode 100644
index 00000000..59190b46
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-contoller-v2-base.ts
@@ -0,0 +1,77 @@
+import moment, { Moment } from "moment";
+import WorklenzControllerBase from "../worklenz-controller-base";
+import momentTime from "moment-timezone";
+
+export const GroupBy = {
+ STATUS: "status",
+ PRIORITY: "priority",
+ LABELS: "labels",
+ PHASE: "phase"
+};
+
+export interface IRMTaskGroup {
+ id?: string;
+ name: string;
+ color_code: string;
+ category_id: string | null;
+ old_category_id?: string;
+ tasks: any[];
+ is_expanded: boolean;
+}
+
+export default class RoadmapTasksControllerV2Base extends WorklenzControllerBase {
+
+ public static updateTaskViewModel(task: any, globalStartDate: Moment, globalDateWidth: number , timeZone: string) {
+
+ if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
+
+ task.is_sub_task = !!task.parent_task_id;
+
+ task.show_sub_tasks = false;
+
+ if (task.start_date)
+ task.start_date = momentTime.tz(task.start_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (task.end_date)
+ task.end_date = momentTime.tz(task.end_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ this.setTaskCss(task, globalStartDate, globalDateWidth);
+
+ task.isVisible = true;
+
+ return task;
+ }
+
+ private static setTaskCss(task: any, globalStartDate: Moment, globalDateWidth: number ) {
+ let startDate = task.start_date ? moment(task.start_date).format("YYYY-MM-DD") : moment();
+ let endDate = task.end_date ? moment(task.end_date).format("YYYY-MM-DD") : moment();
+
+ if (!task.start_date) {
+ startDate = moment(task.end_date).format("YYYY-MM-DD");
+ }
+ if (!task.end_date) {
+ endDate = moment(task.start_date).format("YYYY-MM-DD");
+ }
+ if (!task.start_date && !task.end_date) {
+ startDate = moment().format("YYYY-MM-DD");
+ endDate = moment().format("YYYY-MM-DD");
+ }
+
+ const fStartDate = moment(startDate);
+ const fEndDate = moment(endDate);
+ const fGlobalStartDate = moment(globalStartDate).format("YYYY-MM-DD");
+
+ const daysDifferenceFromStart = fStartDate.diff(fGlobalStartDate, "days");
+ task.offset_from = daysDifferenceFromStart * globalDateWidth;
+
+ if (moment(fStartDate).isSame(moment(fEndDate), "day")) {
+ task.width = globalDateWidth;
+ } else {
+ const taskWidth = fEndDate.diff(fStartDate, "days");
+ task.width = (taskWidth + 1) * globalDateWidth;
+ }
+
+ return task;
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-controller-v2.ts b/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-controller-v2.ts
new file mode 100644
index 00000000..a5a458fa
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-roadmap/roadmap-tasks-controller-v2.ts
@@ -0,0 +1,353 @@
+import {ParsedQs} from "qs";
+
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../../interfaces/worklenz-response";
+import {ServerResponse} from "../../models/server-response";
+import {TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED} from "../../shared/constants";
+import {getColor} from "../../shared/utils";
+import RoadmapTasksControllerV2Base, {GroupBy, IRMTaskGroup} from "./roadmap-tasks-contoller-v2-base";
+import moment, {Moment} from "moment";
+import momentTime from "moment-timezone";
+
+export class TaskListGroup implements IRMTaskGroup {
+ name: string;
+ category_id: string | null;
+ color_code: string;
+ tasks: any[];
+ is_expanded: boolean;
+
+ constructor(group: any) {
+ this.name = group.name;
+ this.category_id = group.category_id || null;
+ this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
+ this.tasks = [];
+ this.is_expanded = group.is_expanded;
+ }
+}
+
+export default class RoadmapTasksControllerV2 extends RoadmapTasksControllerV2Base {
+
+ private static GLOBAL_DATE_WIDTH = 35;
+ private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
+ private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
+
+ private static async getFirstLastDates(projectId: string) {
+
+ const q = `SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(start_date) AS min_date, MAX(start_date) AS max_date
+ FROM tasks
+ WHERE project_id = $1 AND tasks.archived IS FALSE
+ UNION
+ SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
+ FROM tasks
+ WHERE project_id = $1 AND tasks.archived IS FALSE) AS date_union;`;
+
+ const res = await db.query(q, [projectId]);
+ return res.rows[0];
+ }
+
+ private static validateEndDate(endDate: Moment): boolean {
+ return moment(endDate.format("YYYY-MM-DD")).isBefore(moment(), "day");
+ }
+
+ private static validateStartDate(startDate: Moment): boolean {
+ return moment(startDate.format("YYYY-MM-DD")).isBefore(moment(), "day");
+ }
+
+ private static getScrollAmount(startDate: Moment) {
+ const today = moment().format("YYYY-MM-DD");
+ const daysDifference = moment(today).diff(startDate, "days");
+
+ return (this.GLOBAL_DATE_WIDTH * daysDifference);
+ }
+
+ @HandleExceptions()
+ public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const dateRange = await this.getFirstLastDates(req.params.id as string);
+
+ const today = new Date();
+
+ let startDate = moment(today).clone().startOf("month");
+ let endDate = moment(today).clone().endOf("month");
+
+ if (dateRange.start_date)
+ dateRange.start_date = momentTime.tz(dateRange.start_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
+
+ if (dateRange.end_date)
+ dateRange.end_date = momentTime.tz(dateRange.end_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
+
+ if (dateRange.start_date && dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
+ } else if (dateRange.start_date && !dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
+ } else if (!dateRange.start_date && dateRange.end_date) {
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
+ }
+
+ const xMonthsBeforeStart = startDate.clone().subtract(2, "months");
+ const xMonthsAfterEnd = endDate.clone().add(3, "months");
+
+ this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
+ this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
+
+ const dateData = [];
+ let days = -1;
+
+ const currentDate = xMonthsBeforeStart.clone();
+
+ while (currentDate.isBefore(xMonthsAfterEnd)) {
+ const monthData = {
+ month: currentDate.format("MMM YYYY"),
+ weeks: [] as number[],
+ days: [] as { day: number, name: string, isWeekend: boolean, isToday: boolean }[],
+ };
+ const daysInMonth = currentDate.daysInMonth();
+ for (let day = 1; day <= daysInMonth; day++) {
+ const dayOfMonth = currentDate.date();
+ const dayName = currentDate.format("ddd");
+ const isWeekend = [0, 6].includes(currentDate.day());
+ const isToday = moment(moment(today).format("YYYY-MM-DD")).isSame(moment(currentDate).format("YYYY-MM-DD"));
+ monthData.days.push({day: dayOfMonth, name: dayName, isWeekend, isToday});
+ currentDate.add(1, "day");
+ days++;
+ }
+ dateData.push(monthData);
+ }
+
+ const scrollBy = this.getScrollAmount(xMonthsBeforeStart);
+
+ const result = {
+ date_data: dateData,
+ width: days + 1,
+ scroll_by: scrollBy,
+ chart_start: moment(this.GLOBAL_START_DATE).format("YYYY-MM-DD"),
+ chart_end: moment(this.GLOBAL_END_DATE).format("YYYY-MM-DD")
+ };
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ private static isCountsOnly(query: ParsedQs) {
+ return query.count === "true";
+ }
+
+ public static isTasksOnlyReq(query: ParsedQs) {
+ return RoadmapTasksControllerV2.isCountsOnly(query) || query.parent_task;
+ }
+
+
+ private static getQuery(userId: string, options: ParsedQs) {
+ const searchField = options.search ? "t.name" : "sort_order";
+ const {searchQuery} = RoadmapTasksControllerV2.toPaginationOptions(options, searchField);
+
+ const isSubTasks = !!options.parent_task;
+
+ const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
+
+ let subTasksFilter;
+
+ if (options.isSubtasksInclude === "true") {
+ subTasksFilter = "";
+ } else {
+ subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
+ }
+
+ const filters = [
+ subTasksFilter,
+ (isSubTasks ? "1 = 1" : archivedFilter),
+ ].filter(i => !!i).join(" AND ");
+
+ return `
+ SELECT id,
+ name,
+ t.project_id AS project_id,
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
+
+ t.status_id AS status,
+ t.archived,
+
+ (SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
+
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
+
+ (CASE
+ WHEN EXISTS(SELECT 1
+ FROM tasks_with_status_view
+ WHERE tasks_with_status_view.task_id = t.id
+ AND is_done IS TRUE) THEN 1
+ ELSE 0 END) AS parent_task_completed,
+
+ (SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE tt.parent_task_id = t.id
+ AND tt.is_done IS TRUE)::INT
+ AS completed_sub_tasks,
+
+ (SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
+ (SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
+ start_date,
+ end_date
+ FROM tasks t
+ WHERE ${filters} ${searchQuery} AND project_id = $1
+ ORDER BY t.start_date ASC NULLS LAST`;
+ }
+
+ public static async getGroups(groupBy: string, projectId: string): Promise {
+ let q = "";
+ let params: any[] = [];
+ switch (groupBy) {
+ case GroupBy.STATUS:
+ q = `
+ SELECT id,
+ name,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id),
+ category_id
+ FROM task_statuses
+ WHERE project_id = $1
+ ORDER BY sort_order;
+ `;
+ params = [projectId];
+ break;
+ case GroupBy.PRIORITY:
+ q = `SELECT id, name, color_code
+ FROM task_priorities
+ ORDER BY value DESC;`;
+ break;
+ case GroupBy.LABELS:
+ q = `
+ SELECT id, name, color_code
+ FROM team_labels
+ WHERE team_id = $2
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE project_id = $1
+ AND EXISTS(SELECT 1 FROM task_labels WHERE task_id = tasks.id AND label_id = team_labels.id))
+ ORDER BY name;
+ `;
+ break;
+ case GroupBy.PHASE:
+ q = `
+ SELECT id, name, color_code, start_date, end_date
+ FROM project_phases
+ WHERE project_id = $1
+ ORDER BY name;
+ `;
+ params = [projectId];
+ break;
+
+ default:
+ break;
+ }
+
+ const result = await db.query(q, params);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const groupBy = (req.query.group || GroupBy.STATUS) as string;
+
+ const q = RoadmapTasksControllerV2.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+
+ const result = await db.query(q, params);
+ const tasks = [...result.rows];
+
+ const groups = await this.getGroups(groupBy, req.params.id);
+
+ const map = groups.reduce((g: { [x: string]: IRMTaskGroup }, group) => {
+ if (group.id)
+ g[group.id] = new TaskListGroup(group);
+ return g;
+ }, {});
+
+ this.updateMapByGroup(tasks, groupBy, map, req.query.expandedGroups as string, req.query.timezone as string);
+
+ const updatedGroups = Object.keys(map).map(key => {
+ const group = map[key];
+
+ if (groupBy === GroupBy.PHASE)
+ group.color_code = getColor(group.name) + TASK_PRIORITY_COLOR_ALPHA;
+
+ return {
+ id: key,
+ ...group
+ };
+ });
+
+ if (req.query.expandedGroups) {
+ const expandedGroup = updatedGroups.find(g => g.id === req.query.expandedGroups);
+ if (expandedGroup) expandedGroup.is_expanded = true;
+ } else {
+ updatedGroups[0].is_expanded = true;
+ }
+
+ return res.status(200).send(new ServerResponse(true, updatedGroups));
+ }
+
+ public static updateMapByGroup(tasks: any[], groupBy: string, map: {
+ [p: string]: IRMTaskGroup
+ }, expandedGroup: string, timeZone: string) {
+ let index = 0;
+ const unmapped = [];
+ for (const task of tasks) {
+ task.index = index++;
+ RoadmapTasksControllerV2.updateTaskViewModel(task, moment(this.GLOBAL_START_DATE), this.GLOBAL_DATE_WIDTH, timeZone);
+ if (groupBy === GroupBy.STATUS) {
+ map[task.status]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PRIORITY) {
+ map[task.priority]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PHASE && task.phase_id) {
+ map[task.phase_id]?.tasks.push(task);
+ } else {
+ unmapped.push(task);
+ }
+ }
+
+ if (unmapped.length) {
+ map[UNMAPPED] = {
+ name: UNMAPPED,
+ category_id: null,
+ color_code: "#f0f0f0",
+ tasks: unmapped,
+ is_expanded: false
+ };
+ }
+ }
+
+
+ @HandleExceptions()
+ public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const q = RoadmapTasksControllerV2.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+ const result = await db.query(q, params);
+
+ let data: any[] = [];
+
+ // if true, we only return the record count
+ if (this.isCountsOnly(req.query)) {
+ [data] = result.rows;
+ } else { // else we return a flat list of tasks
+ data = [...result.rows];
+ for (const task of data) {
+ RoadmapTasksControllerV2.updateTaskViewModel(task, moment(this.GLOBAL_START_DATE), this.GLOBAL_DATE_WIDTH, req.query.timeZone as string);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-statuses-controller.ts b/worklenz-backend/src/controllers/project-statuses-controller.ts
new file mode 100644
index 00000000..9d2d1271
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-statuses-controller.ts
@@ -0,0 +1,16 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class ProjectstatusesController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, color_code, icon, is_default FROM sys_project_statuses ORDER BY sort_order;`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-templates/interfaces.ts b/worklenz-backend/src/controllers/project-templates/interfaces.ts
new file mode 100644
index 00000000..b3c6edf8
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/interfaces.ts
@@ -0,0 +1,88 @@
+export interface IProjectTemplateLabel {
+ label_id?: string;
+ name?: string;
+ color_code?: string;
+}
+
+export interface IProjectTemplate {
+ name?: string;
+ id?: string;
+ key?: string;
+ description?: string;
+ phase_label?: string;
+ phases?: any;
+ tasks?: any;
+ status?: any;
+}
+
+export interface IProjectTemplatePhase {
+ id?: string;
+ name?: string;
+ color_code?: string;
+}
+
+export interface IProjectTemplateStatus {
+ id?: string;
+ name?: string;
+ category_id?: string;
+ category_name?: string;
+ sort_order?: string;
+}
+
+export interface IProjectTaskPhase {
+ name?: string;
+}
+
+export interface IProjectTemplateTask {
+ id?: string;
+ name?: string;
+ description?: string | null;
+ total_minutes?: number;
+ sort_order?: number;
+ priority_id?: string;
+ priority_name?: string;
+ new?: number;
+ parent_task_id?: string | null;
+ status_id?: string;
+ status_name?: string;
+ phase_id?: string;
+ phase_name?: string;
+ phases?: IProjectTaskPhase[];
+ labels?: IProjectTemplateLabel[];
+ task_no?: number;
+ original_task_id?: string;
+}
+
+export interface ITaskIncludes {
+ status?: boolean;
+ phase?: boolean;
+ labels?: boolean;
+ estimation?: boolean;
+ description?: boolean;
+ subtasks?: boolean;
+}
+
+export interface ICustomProjectTemplate {
+ name?: string;
+ phase_label?: string;
+ color_code?: string;
+ notes?: string;
+ team_id?: string;
+}
+
+export interface ICustomTemplatePhase {
+ name?: string;
+ color_code?: string;
+ template_id?: string;
+}
+
+export interface ICustomTemplateTask {
+ name?: string;
+ description: string;
+ total_minutes: string;
+ sort_order: string;
+ priority_id: string;
+ template_id: string;
+ parent_task_id: string;
+ status_id?: string;
+}
diff --git a/worklenz-backend/src/controllers/project-templates/project-templates-base.ts b/worklenz-backend/src/controllers/project-templates/project-templates-base.ts
new file mode 100644
index 00000000..2c7a452b
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/project-templates-base.ts
@@ -0,0 +1,521 @@
+import { Socket } from "socket.io";
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { logStatusChange } from "../../services/activity-logs/activity-logs.service";
+import { getColor, int, log_error } from "../../shared/utils";
+import { generateProjectKey } from "../../utils/generate-project-key";
+import WorklenzControllerBase from "../worklenz-controller-base";
+import { ICustomProjectTemplate, ICustomTemplatePhase, IProjectTemplate, IProjectTemplateLabel, IProjectTemplatePhase, IProjectTemplateStatus, IProjectTemplateTask, ITaskIncludes } from "./interfaces";
+
+export default abstract class ProjectTemplatesControllerBase extends WorklenzControllerBase {
+
+ @HandleExceptions()
+ protected static async insertProjectTemplate(body: IProjectTemplate) {
+ const { name, key, description, phase_label } = body;
+
+ const q = `INSERT INTO pt_project_templates(name, key, description, phase_label) VALUES ($1, $2, $3, $4) RETURNING id;`;
+ const result = await db.query(q, [name, key, description, phase_label]);
+ const [data] = result.rows;
+ return data.id;
+ }
+
+ @HandleExceptions()
+ protected static async insertTemplateProjectPhases(body: IProjectTemplatePhase[], template_id: string) {
+ for await (const phase of body) {
+ const { name, color_code } = phase;
+
+ const q = `INSERT INTO pt_phases(name, color_code, template_id) VALUES ($1, $2, $3);`;
+ await db.query(q, [name, color_code, template_id]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertTemplateProjectStatuses(body: IProjectTemplateStatus[], template_id: string) {
+ for await (const status of body) {
+ const { name, category_name, category_id } = status;
+
+ const q = `INSERT INTO pt_statuses(name, template_id, category_id)
+ VALUES ($1, $2, (SELECT id FROM sys_task_status_categories WHERE sys_task_status_categories.name = $3));`;
+ await db.query(q, [name, template_id, category_name]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertTemplateProjectTasks(body: IProjectTemplateTask[], template_id: string) {
+ for await (const template_task of body) {
+ const { name, description, total_minutes, sort_order, priority_name, parent_task_id, phase_name, status_name } = template_task;
+
+ const q = `INSERT INTO pt_tasks(name, description, total_minutes, sort_order, priority_id, template_id, parent_task_id, status_id)
+ VALUES ($1, $2, $3, $4, (SELECT id FROM task_priorities WHERE task_priorities.name = $5), $6, $7,
+ (SELECT id FROM pt_statuses WHERE pt_statuses.name = $8 AND pt_statuses.template_id = $6)) RETURNING id;`;
+ const result = await db.query(q, [name, description, total_minutes, sort_order, priority_name, template_id, parent_task_id, status_name]);
+ const [task] = result.rows;
+
+ await this.insertTemplateTaskPhases(task.id, template_id, phase_name);
+ if (template_task.labels) await this.insertTemplateTaskLabels(task.id, template_task.labels);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertTemplateTaskPhases(task_id: string, template_id: string, phase_name = "") {
+ const q = `INSERT INTO pt_task_phases (task_id, phase_id) VALUES ($1, (SELECT id FROM pt_phases WHERE template_id = $2 AND name = $3));`;
+ await db.query(q, [task_id, template_id, phase_name]);
+ }
+
+ @HandleExceptions()
+ protected static async insertTemplateTaskLabels(task_id: string, labels: IProjectTemplateLabel[]) {
+ for await (const label of labels) {
+ const q = `INSERT INTO pt_task_labels(task_id, label_id) VALUES ($1, (SELECT id FROM pt_labels WHERE name = $2));`;
+ await db.query(q, [task_id, label.name]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async getTemplateData(template_id: string) {
+ const q = `SELECT id,
+ name,
+ description,
+ phase_label,
+ image_url,
+ color_code,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name, color_code FROM pt_phases WHERE template_id = pt.id) rec) AS phases,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name,
+ category_id,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.id = pt_statuses.category_id)
+ FROM pt_statuses
+ WHERE template_id = pt.id) rec) AS status,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name, pt_labels.color_code
+ FROM pt_labels
+ WHERE id IN (SELECT label_id
+ FROM pt_task_labels pttl
+ WHERE task_id IN (SELECT id
+ FROM pt_tasks
+ WHERE pt_tasks.template_id = pt.id))) rec) AS labels,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name,
+ color_code
+ FROM task_priorities) rec) AS priorities,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name,
+ (SELECT name FROM pt_statuses WHERE status_id = pt_statuses.id) AS status_name,
+ (SELECT name FROM task_priorities tp WHERE priority_id = tp.id ) AS priority_name,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name
+ FROM pt_phases pl
+ WHERE pl.id =
+ (SELECT phase_id FROM pt_task_phases WHERE task_id = pt_tasks.id)) rec) AS phases,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name
+ FROM pt_labels pl
+ LEFT JOIN pt_task_labels pttl ON pl.id = pttl.label_id
+ WHERE pttl.task_id = pt_tasks.id) rec) AS labels
+ FROM pt_tasks
+ WHERE template_id = pt.id) rec) AS tasks
+ FROM pt_project_templates pt
+ WHERE id = $1;`;
+ const result = await db.query(q, [template_id]);
+ const [data] = result.rows;
+ for (const phase of data.phases) {
+ phase.color_code = getColor(phase.name);
+ }
+ return data;
+ }
+
+ @HandleExceptions()
+ protected static async getCustomTemplateData(template_id: string) {
+ const q = `SELECT id,
+ name,
+ notes AS description,
+ phase_label,
+ color_code,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name, color_code FROM cpt_phases WHERE template_id = pt.id) rec) AS phases,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name,
+ category_id,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE sys_task_status_categories.id = cpts.category_id)
+ FROM cpt_task_statuses cpts
+ WHERE template_id = pt.id ORDER BY sort_order) rec) AS status,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name, tl.color_code
+ FROM team_labels tl
+ WHERE id IN (SELECT label_id
+ FROM cpt_task_labels ctl
+ WHERE task_id IN (SELECT id
+ FROM cpt_tasks
+ WHERE cpt_tasks.template_id = pt.id))) rec) AS labels,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name,
+ color_code
+ FROM task_priorities) rec) AS priorities,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT id AS original_task_id,
+ name,
+ parent_task_id,
+ description,
+ total_minutes,
+ (SELECT name FROM cpt_task_statuses cts WHERE status_id = cts.id) AS status_name,
+ (SELECT name FROM task_priorities tp WHERE priority_id = tp.id) AS priority_name,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name
+ FROM cpt_phases pl
+ WHERE pl.id =
+ (SELECT phase_id FROM cpt_task_phases WHERE task_id = cpt_tasks.id)) rec) AS phases,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT name
+ FROM team_labels pl
+ LEFT JOIN cpt_task_labels cttl ON pl.id = cttl.label_id
+ WHERE cttl.task_id = cpt_tasks.id) rec) AS labels
+ FROM cpt_tasks
+ WHERE template_id = pt.id
+ ORDER BY parent_task_id NULLS FIRST) rec) AS tasks
+ FROM custom_project_templates pt
+ WHERE id = $1;`;
+ const result = await db.query(q, [template_id]);
+ const [data] = result.rows;
+ return data;
+ }
+
+ private static async getAllKeysByTeamId(teamId?: string) {
+ if (!teamId) return [];
+ try {
+ const result = await db.query("SELECT key FROM projects WHERE team_id = $1;", [teamId]);
+ return result.rows.map((project: any) => project.key).filter((key: any) => !!key);
+ } catch (error) {
+ return [];
+ }
+ }
+
+ private static async checkProjectNameExists(project_name: string, teamId?: string) {
+ if (!teamId) return;
+ try {
+ const result = await db.query("SELECT count(*) FROM projects WHERE name = $1 AND team_id = $2;", [project_name, teamId]);
+ const [data] = result.rows;
+ return int(data.count) || 0;
+ } catch (error) {
+ return [];
+ }
+ }
+
+ @HandleExceptions()
+ protected static async importTemplate(body: any) {
+ const q = `SELECT create_project($1) AS project`;
+
+ const count = await this.checkProjectNameExists(body.name, body.team_id);
+
+ const keys = await this.getAllKeysByTeamId(body.team_id as string);
+ body.key = generateProjectKey(body.name, keys) || null;
+
+ if (count !== 0) body.name = `${body.name} - ${body.key}`;
+
+ const result = await db.query(q, [JSON.stringify(body)]);
+ const [data] = result.rows;
+
+ return data.project.id;
+ }
+
+ @HandleExceptions()
+ protected static async insertTeamLabels(labels: IProjectTemplateLabel[], team_id = "") {
+ if (!team_id) return;
+
+ for await (const label of labels) {
+ const q = `INSERT INTO team_labels(name, color_code, team_id)
+ VALUES ($1, $2, $3)
+ ON CONFLICT (name, team_id) DO NOTHING;`;
+ await db.query(q, [label.name, label.color_code, team_id]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertProjectPhases(phases: IProjectTemplatePhase[], project_id = "",) {
+ if (!project_id) return;
+
+ let i = 0;
+
+ for await (const phase of phases) {
+ const q = `INSERT INTO project_phases(name, color_code, project_id, sort_index) VALUES ($1, $2, $3, $4);`;
+ await db.query(q, [phase.name, phase.color_code, project_id, i]);
+ i++;
+ }
+ }
+
+ protected static async insertProjectStatuses(statuses: IProjectTemplateStatus[], project_id = "", team_id = "") {
+ if (!project_id || !team_id) return;
+
+ try {
+ for await (const status of statuses) {
+ const q = `INSERT INTO task_statuses(name, project_id, team_id, category_id) VALUES($1, $2, $3, $4);`;
+ await db.query(q, [status.name, project_id, team_id, status.category_id]);
+ }
+ } catch (error) {
+ log_error(error);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertTaskPhase(task_id: string, phase_name: string, project_id: string) {
+ const q = `INSERT INTO task_phase(task_id, phase_id)
+ VALUES ($1, (SELECT id FROM project_phases WHERE name = $2 AND project_id = $3));`;
+ await db.query(q, [task_id, phase_name, project_id]);
+ }
+
+ @HandleExceptions()
+ protected static async insertTaskLabel(task_id: string, label_name: string, team_id: string) {
+ const q = `INSERT INTO task_labels(task_id, label_id)
+ VALUES ($1, (SELECT id FROM team_labels WHERE name = $2 AND team_id = $3));`;
+ await db.query(q, [task_id, label_name, team_id]);
+ }
+
+ protected static async insertProjectTasks(tasks: IProjectTemplateTask[], team_id: string, project_id = "", user_id = "", socket: Socket | null) {
+ if (!project_id) return;
+
+ try {
+ for await (const [key, task] of tasks.entries()) {
+ const q = `INSERT INTO tasks(name, project_id, status_id, priority_id, reporter_id, sort_order)
+ VALUES ($1, $2, (SELECT id FROM task_statuses ts WHERE ts.name = $3 AND ts.project_id = $2),
+ (SELECT id FROM task_priorities tp WHERE tp.name = $4), $5, $6)
+ RETURNING id, status_id;`;
+ const result = await db.query(q, [task.name, project_id, task.status_name, task.priority_name, user_id, key]);
+ const [data] = result.rows;
+
+ if (task.phases) {
+ for await (const phase of task.phases) {
+ await this.insertTaskPhase(data.id, phase.name as string, project_id);
+ }
+ }
+
+ if (task.labels) {
+ for await (const label of task.labels) {
+ await this.insertTaskLabel(data.id, label.name as string, team_id);
+ }
+ }
+
+ if (socket) {
+ logStatusChange({
+ task_id: data.id,
+ socket,
+ new_value: data.status_id,
+ old_value: null
+ });
+ }
+
+ }
+ } catch (error) {
+ log_error(error);
+ }
+ }
+
+ // custom templates
+ @HandleExceptions()
+ protected static async getProjectData(project_id: string) {
+ const q = `SELECT phase_label, notes, color_code FROM projects WHERE id = $1;`;
+ const result = await db.query(q, [project_id]);
+ const [data] = result.rows;
+ return data;
+ }
+
+ @HandleExceptions()
+ protected static async getProjectStatus(project_id: string) {
+ const q = `SELECT name, category_id, sort_order FROM task_statuses WHERE project_id = $1;`;
+ const result = await db.query(q, [project_id]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ protected static async getProjectPhases(project_id: string) {
+ const q = `SELECT name, color_code FROM project_phases WHERE project_id = $1 ORDER BY sort_index ASC;`;
+ const result = await db.query(q, [project_id]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ protected static async getProjectLabels(team_id: string, project_id: string) {
+ const q = `SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(DISTINCT JSONB_BUILD_OBJECT('name', name))), '[]'::JSON) AS labels
+ FROM team_labels
+ WHERE team_id = $1
+ AND id IN (SELECT label_id
+ FROM task_labels
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id = $2));`;
+ const result = await db.query(q, [team_id, project_id]);
+ const [data] = result.rows;
+ return data.labels;
+ }
+
+ @HandleExceptions()
+ protected static async getTasksByProject(project_id: string, taskIncludes: ITaskIncludes) {
+ let taskIncludesClause = "";
+
+ if (taskIncludes.description) taskIncludesClause += " description,";
+ if (taskIncludes.estimation) taskIncludesClause += " total_minutes,";
+ if (taskIncludes.status) taskIncludesClause += ` (SELECT name FROM task_statuses WHERE status_id = id) AS status_name,`;
+ if (taskIncludes.labels) {
+ taskIncludesClause += ` (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT (SELECT name FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = t.id) rec) AS labels,`;
+ }
+ if (taskIncludes.phase) {
+ taskIncludesClause += ` (SELECT name
+ FROM project_phases
+ WHERE project_phases.id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,`;
+ }
+ if (taskIncludes.subtasks) {
+ taskIncludesClause += ` parent_task_id,`;
+ }
+
+ const q = `SELECT id,
+ name,
+ sort_order,
+ task_no,
+ ${taskIncludesClause}
+ priority_id
+ FROM tasks t
+ WHERE project_id = $1
+ AND archived IS FALSE ORDER BY parent_task_id NULLS FIRST;`;
+ const result = await db.query(q, [project_id]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplate(body: ICustomProjectTemplate) {
+ const q = `SELECT create_project_template($1)`;
+ const result = await db.query(q, [JSON.stringify(body)]);
+ const [data] = result.rows;
+ return data.id;
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplatePhases(body: ICustomTemplatePhase[], template_id: string) {
+ for await (const phase of body) {
+ const { name, color_code } = phase;
+
+ const q = `INSERT INTO cpt_phases(name, color_code, template_id) VALUES ($1, $2, $3);`;
+ await db.query(q, [name, color_code, template_id]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplateStatus(body: IProjectTemplateStatus[], template_id: string, team_id: string) {
+ for await (const status of body) {
+ const { name, category_id, sort_order } = status;
+
+ const q = `INSERT INTO cpt_task_statuses(name, template_id, team_id, category_id, sort_order)
+ VALUES ($1, $2, $3, $4, $5);`;
+ await db.query(q, [name, template_id, team_id, category_id, sort_order]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplateTasks(body: IProjectTemplateTask[], template_id: string, team_id: string, status = true) {
+ for await (const task of body) {
+ const { name, description, total_minutes, sort_order, priority_id, status_name, task_no, parent_task_id, id, phase_name } = task;
+
+ const q = `INSERT INTO cpt_tasks(name, description, total_minutes, sort_order, priority_id, template_id, status_id, task_no,
+ parent_task_id, original_task_id)
+ VALUES ($1, $2, $3, $4, $5, $6, (SELECT id FROM cpt_task_statuses cts WHERE cts.name = $7 AND cts.template_id = $6), $8,
+ (SELECT id FROM cpt_tasks WHERE original_task_id = $9 AND template_id = $6), $10)
+ RETURNING id;`;
+ const result = await db.query(q, [name, description, total_minutes || 0, sort_order, priority_id, template_id, status_name, task_no, parent_task_id, id]);
+ const [data] = result.rows;
+
+ if (data.id) {
+ if (phase_name) await this.insertCustomTemplateTaskPhases(data.id, template_id, phase_name);
+ if (task.labels) await this.insertCustomTemplateTaskLabels(data.id, task.labels, team_id);
+ }
+ }
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplateTaskPhases(task_id: string, template_id: string, phase_name = "") {
+ const q = `INSERT INTO cpt_task_phases (task_id, phase_id)
+ VALUES ($1, (SELECT id FROM cpt_phases WHERE template_id = $2 AND name = $3));`;
+ await db.query(q, [task_id, template_id, phase_name]);
+ }
+
+ @HandleExceptions()
+ protected static async insertCustomTemplateTaskLabels(task_id: string, labels: IProjectTemplateLabel[], team_id: string) {
+ for await (const label of labels) {
+ const q = `INSERT INTO cpt_task_labels(task_id, label_id)
+ VALUES ($1, (SELECT id FROM team_labels WHERE name = $2 AND team_id = $3));`;
+ await db.query(q, [task_id, label.name, team_id]);
+ }
+ }
+
+ @HandleExceptions()
+ protected static async updateTeamName(name: string, team_id: string, user_id: string) {
+ const q = `UPDATE teams SET name = TRIM($1::TEXT) WHERE id = $2 AND user_id = $3;`;
+ const result = await db.query(q, [name, team_id, user_id]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ protected static async deleteDefaultStatusForProject(task_id: string) {
+ const q = `DELETE FROM task_statuses WHERE project_id = $1;`;
+ await db.query(q, [task_id]);
+ }
+
+
+ @HandleExceptions()
+ protected static async handleAccountSetup(project_id: string, user_id: string, team_name: string) {
+ // update user setup status
+ await db.query(`UPDATE users SET setup_completed = TRUE WHERE id = $1;`, [user_id]);
+
+ await db.query(`INSERT INTO organizations (user_id, organization_name, contact_number, contact_number_secondary, trial_in_progress,
+ trial_expire_date, subscription_status)
+ VALUES ($1, TRIM($2::TEXT), NULL, NULL, TRUE, CURRENT_DATE + INTERVAL '14 days', 'trialing')
+ ON CONFLICT (user_id) DO UPDATE SET organization_name = TRIM($2::TEXT);`, [user_id, team_name]);
+ }
+
+ protected static async insertProjectTasksFromCustom(tasks: IProjectTemplateTask[], team_id: string, project_id = "", user_id = "", socket: Socket | null) {
+ if (!project_id) return;
+
+ try {
+ for await (const [key, task] of tasks.entries()) {
+ const q = `INSERT INTO tasks(name, project_id, status_id, priority_id, reporter_id, sort_order, parent_task_id, description, total_minutes)
+ VALUES ($1, $2, (SELECT id FROM task_statuses ts WHERE ts.name = $3 AND ts.project_id = $2),
+ (SELECT id FROM task_priorities tp WHERE tp.name = $4), $5, $6, $7, $8, $9)
+ RETURNING id, status_id;`;
+
+ const parent_task: IProjectTemplateTask = tasks.find(t => t.original_task_id === task.parent_task_id) || {};
+
+ const result = await db.query(q, [task.name, project_id, task.status_name, task.priority_name, user_id, key, parent_task.id, task.description, task.total_minutes ? task.total_minutes : 0]);
+ const [data] = result.rows;
+ task.id = data.id;
+
+ if (task.phases) {
+ for await (const phase of task.phases) {
+ await this.insertTaskPhase(data.id, phase.name as string, project_id);
+ }
+ }
+
+ if (task.labels) {
+ for await (const label of task.labels) {
+ await this.insertTaskLabel(data.id, label.name as string, team_id);
+ }
+ }
+
+ if (socket) {
+ logStatusChange({
+ task_id: data.id,
+ socket,
+ new_value: data.status_id,
+ old_value: null
+ });
+ }
+
+ }
+ } catch (error) {
+ log_error(error);
+ }
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-templates/project-templates.ts b/worklenz-backend/src/controllers/project-templates/project-templates.ts
new file mode 100644
index 00000000..d67fdae9
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/project-templates.ts
@@ -0,0 +1,152 @@
+export const templateData = [
+ {
+ "id": "a07c96df-b74f-41ce-8225-9174bcca85f2",
+ "name": "Bug Tracking",
+ "key": "BT",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "7e417356-c62b-43cd-b69c-12109156fdea", "name": "Testing and Verification", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "fbe8cdc3-770b-48ba-86f2-8515b4bc6590", "status_name": "To Do", "phase_id": "c8fce20d-59e9-47f2-9adc-cdae45d205fd", "phase_name": "Resolved", "labels": [{ "label_id": "1eb9009d-8e69-4143-a742-1c52167323c5", "name": "Critical" }] }, { "id": "daba3b23-233b-486b-bc09-0826edbd9083", "name": "Bug Prioritization", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "79bc360d-0145-497b-8224-e76421c36ed7", "status_name": "Doing", "phase_id": "a5a1c81f-09a7-46b9-8af8-970d627eda97", "phase_name": "Development work", "labels": [{ "label_id": "514965aa-9d4d-4cdc-bda1-e5bd35445c2a", "name": "Awaiting review" }] }, { "id": "2d66bc17-ea2b-4194-9d2a-299501ab58e9", "name": "Bug reporting", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "fbe8cdc3-770b-48ba-86f2-8515b4bc6590", "status_name": "To Do", "phase_id": "c46616ea-9c98-4c11-be8e-100e8b55dcff", "phase_name": "Backlog", "labels": [{ "label_id": "75ba3fc1-03f3-4a48-a9a8-e85aee5235e7", "name": "Duplicate" }] }, { "id": "cb386cf5-28a2-4e34-9eb4-0a3cb6262107", "name": "Bug Assignment", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "79bc360d-0145-497b-8224-e76421c36ed7", "status_name": "Doing", "phase_id": "8db0540e-f064-4970-b3f0-60e4357b29b9", "phase_name": "Incoming", "labels": [{ "label_id": "5f9ce7dd-c9ce-4db6-968b-692c6bcae608", "name": "UI/UX Bug" }, { "label_id": "8a7afd6d-db50-4aa9-985a-4da4216eaafa", "name": "Regression" }] }, { "id": "94f859d8-a739-4964-9bf5-4fec8c5f44c5", "name": "Bug Closure", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "815acc59-9296-4b49-93af-5dd663c3aa52", "status_name": "Done", "phase_id": "0b55c1cb-62b3-414c-a8ff-b952168b4e91", "phase_name": "Testing & Review", "labels": [{ "label_id": "2e474783-0453-475f-a329-62e9530a8265", "name": "Ready for Dev" }] }, { "id": "dbbe180c-0dd5-492a-8f67-82a320d90ada", "name": "Documentation", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "815acc59-9296-4b49-93af-5dd663c3aa52", "status_name": "Done", "phase_id": "c46616ea-9c98-4c11-be8e-100e8b55dcff", "phase_name": "Backlog", "labels": [{ "label_id": "166b97d0-7d00-4161-8267-cd95c4915f00", "name": "Fixed" }, { "label_id": "7ad6471c-89fb-4fcc-93ff-d3d91aac9846", "name": "Fixing" }] }, { "id": "53c45f94-133d-451d-9d2f-6a0c6cd54321", "name": "Reporting", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "815acc59-9296-4b49-93af-5dd663c3aa52", "status_name": "Done", "phase_id": "c46616ea-9c98-4c11-be8e-100e8b55dcff", "phase_name": "Backlog", "labels": [{ "label_id": "7079771a-9e6d-4190-9e96-3183c16a1e5d", "name": "Documentation" }] }],
+ "phases": [{ "id": "c46616ea-9c98-4c11-be8e-100e8b55dcff", "color_code": "#a9a9a9", "name": "Backlog" }, { "id": "a5a1c81f-09a7-46b9-8af8-970d627eda97", "color_code": "#a9a9a9", "name": "Development work" }, { "id": "8db0540e-f064-4970-b3f0-60e4357b29b9", "color_code": "#a9a9a9", "name": "Incoming" }, { "id": "0b55c1cb-62b3-414c-a8ff-b952168b4e91", "color_code": "#a9a9a9", "name": "Testing & Review" }, { "id": "c8fce20d-59e9-47f2-9adc-cdae45d205fd", "color_code": "#a9a9a9", "name": "Resolved" }],
+ "status": [{ "id": "fbe8cdc3-770b-48ba-86f2-8515b4bc6590", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "79bc360d-0145-497b-8224-e76421c36ed7", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "815acc59-9296-4b49-93af-5dd663c3aa52", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "4e96f0e6-53e9-4ed2-87b0-7310fdc2807a",
+ "name": "Construction",
+ "key": "CON",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "d9a9048c-8aba-4ba3-9fa0-8e28aec9b903", "name": "Insulation and HVAC", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "2dbad7df-89a3-40e9-b535-b3eabc205065", "status_name": "Doing", "phase_id": "661b5e8a-14dc-4e70-8819-0623cc017674", "phase_name": "Design", "labels": [{ "label_id": "025dd865-b983-4254-9599-aaff0a19bbfa", "name": "Environmental" }] }, { "id": "93c7fcc5-838a-412c-9121-408f3997a3a0", "name": "Site Preparation", "description": null, "total_minutes": 0, "sort_order": 8, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 8, "parent_task_id": null, "status_id": "2dbad7df-89a3-40e9-b535-b3eabc205065", "status_name": "Doing", "phase_id": "b10d9622-d466-4dd0-8149-caddb5aa70fb", "phase_name": "Contracts", "labels": [{ "label_id": "78ea945a-6d05-4e31-99a8-4027a9df8bde", "name": "Communication" }] }, { "id": "6f8aa50f-e471-4d17-a6b4-d2d407f8106e", "name": "Electrical and Plumbing", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "2dbad7df-89a3-40e9-b535-b3eabc205065", "status_name": "Doing", "phase_id": "661b5e8a-14dc-4e70-8819-0623cc017674", "phase_name": "Design", "labels": [{ "label_id": "ada7bbbd-4081-45c4-a531-9eaf0814da7e", "name": "Financial" }] }, { "id": "74f0b11e-5ac2-425d-949d-eb50e9cb8aef", "name": "Project Planning", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "df60d582-d962-4f58-a46b-4470e21ad3c6", "status_name": "To Do", "phase_id": "b10d9622-d466-4dd0-8149-caddb5aa70fb", "phase_name": "Contracts", "labels": [{ "label_id": "dfc45c4b-ccd9-4988-9523-6d40e9e839ef", "name": "Budget and Cost Tracking" }] }, { "id": "b51f3cd8-729e-43fd-96c6-24f43e7aee6c", "name": "Exterior Work", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "0860641e-74d5-49ee-9f9a-7e3d103e936b", "status_name": "Done", "phase_id": "78f8af29-82ab-44ac-a46f-67e426e894d2", "phase_name": "Post Construction", "labels": [{ "label_id": "8774a1d6-c7b9-4d9f-a1ab-530a9461846e", "name": "Document" }] }, { "id": "059daea8-2d11-426e-ade9-f8d4e9ab8273", "name": "Structural Framing", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "df60d582-d962-4f58-a46b-4470e21ad3c6", "status_name": "To Do", "phase_id": "31313411-eaa0-4d0f-a652-5dc9147589e2", "phase_name": "Construction", "labels": [{ "label_id": "ad43f647-f2dc-4ccf-af58-73d633045588", "name": "Safety" }] }, { "id": "3565912b-a866-4d16-9d2d-9cd0829bb4b7", "name": "Foundation Work", "description": null, "total_minutes": 0, "sort_order": 9, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 9, "parent_task_id": null, "status_id": "0860641e-74d5-49ee-9f9a-7e3d103e936b", "status_name": "Done", "phase_id": "b10d9622-d466-4dd0-8149-caddb5aa70fb", "phase_name": "Contracts", "labels": [{ "label_id": "165393b6-2e29-49f7-b07c-f94ee2d9e3d1", "name": "Emergency Response" }] }, { "id": "cc2234d1-6919-47a7-8305-7004ae58c4b2", "name": "Finishing and Finishing Work", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "0860641e-74d5-49ee-9f9a-7e3d103e936b", "status_name": "Done", "phase_id": "78f8af29-82ab-44ac-a46f-67e426e894d2", "phase_name": "Post Construction", "labels": [{ "label_id": "dfc45c4b-ccd9-4988-9523-6d40e9e839ef", "name": "Budget and Cost Tracking" }] }, { "id": "8a4e9a89-0555-4c08-b02c-233a21cd0c46", "name": "Quality Assurance and Inspections", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "df60d582-d962-4f58-a46b-4470e21ad3c6", "status_name": "To Do", "phase_id": "59acc68f-30fa-4398-9a44-24b8581cef75", "phase_name": "Procurement", "labels": [{ "label_id": "bcc5a490-e10e-4e03-ab33-80f8ddc0f3bc", "name": "Material" }] }, { "id": "fcb97c39-92c7-4f27-9655-a87c8e6e88f0", "name": "Utilities and Systems Integration", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "df60d582-d962-4f58-a46b-4470e21ad3c6", "status_name": "To Do", "phase_id": "59acc68f-30fa-4398-9a44-24b8581cef75", "phase_name": "Procurement", "labels": [{ "label_id": "8a540fab-3bcb-4ef0-8d93-5d7452966dcb", "name": "Location and Area" }] }],
+ "phases": [{ "id": "31313411-eaa0-4d0f-a652-5dc9147589e2", "color_code": "#a9a9a9", "name": "Construction" }, { "id": "59acc68f-30fa-4398-9a44-24b8581cef75", "color_code": "#a9a9a9", "name": "Procurement" }, { "id": "78f8af29-82ab-44ac-a46f-67e426e894d2", "color_code": "#a9a9a9", "name": "Post Construction" }, { "id": "661b5e8a-14dc-4e70-8819-0623cc017674", "color_code": "#a9a9a9", "name": "Design" }, { "id": "b10d9622-d466-4dd0-8149-caddb5aa70fb", "color_code": "#a9a9a9", "name": "Contracts" }],
+ "status": [{ "id": "df60d582-d962-4f58-a46b-4470e21ad3c6", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "2dbad7df-89a3-40e9-b535-b3eabc205065", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "0860641e-74d5-49ee-9f9a-7e3d103e936b", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "a371a050-4fc1-4fd4-b68a-6335cc3e856f",
+ "name": "Design & Creative",
+ "key": "DC",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "fc940e65-760d-4001-972c-ee5388ae9ec1", "name": "Brainstorm creative ideas and concepts, and sketch initial designs.", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "d84fa4de-d54c-45d7-aba0-9ee34c1348db", "phase_name": "Concepts", "labels": [{ "label_id": "587e1e2f-9c96-460d-8a3a-33fbb744948e", "name": "Ideation and Sketching" }] }, { "id": "e0600990-f5b3-45c9-827d-a8546055d0d9", "name": "Create storyboards or wireframes to outline the structure and flow of the design.", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "d84fa4de-d54c-45d7-aba0-9ee34c1348db", "phase_name": "Concepts", "labels": [{ "label_id": "de261027-5b4a-4265-ae51-a01051bce98d", "name": "Storyboarding or Wireframing" }] }, { "id": "6876eb44-d708-492d-9158-6026cd70d453", "name": "Create user-friendly interfaces for digital products, ensuring a seamless user experience.", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "d84fa4de-d54c-45d7-aba0-9ee34c1348db", "phase_name": "Concepts", "labels": [{ "label_id": "9146e800-8707-4b7d-b5f9-11bdf5e3f9d8", "name": "User Interface (UI) Design" }] }, { "id": "c3ccded5-b653-4fa5-b6d9-8f67c60a09a5", "name": "Develop high-quality visual design assets based on approved concepts.", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "cbad8cd9-5696-4f00-b463-a814e5a1c984", "phase_name": "Design", "labels": [{ "label_id": "d39a1b79-3a74-4876-9c8f-6b78dfde670d", "name": "Graphic Design" }] }, { "id": "649bc87d-86d5-4708-9f73-86789df31eb5", "name": "Prepare design files for final production, including optimizing for different platforms.", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "d947e01c-f7d6-4bc2-80d2-f272e9f5a3f5", "phase_name": "Production", "labels": [{ "label_id": "d7c57e8e-188e-42d8-a638-a2302d2d5dcc", "name": "Production Preparation" }] }, { "id": "5f1bfce6-1bb3-49e3-bf45-41a2e95e149c", "name": "Collaborate with production teams to produce and deliver the final design assets.", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "d947e01c-f7d6-4bc2-80d2-f272e9f5a3f5", "phase_name": "Production", "labels": [{ "label_id": "fb4ea9af-e746-404a-8728-bbdac2466ce3", "name": "Digital Production" }] }, { "id": "b9c448a6-b45e-4960-8170-41b197d25a6d", "name": "Research and analyze the target market, including demographics and trends.", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "c97faab3-5606-4dd3-8f05-42eb6cfedc71", "phase_name": "Research", "labels": [{ "label_id": "b345f733-b10a-42e3-be8a-5809155d0b69", "name": "Market Analysis" }] }, { "id": "a7cdc726-3979-461b-bd9d-ec198f2f10e6", "name": "Study competitors' design strategies and identify opportunities for differentiation.", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "status_name": "To Do", "phase_id": "c97faab3-5606-4dd3-8f05-42eb6cfedc71", "phase_name": "Research", "labels": [{ "label_id": "20a9fca1-55fe-4b8f-b80f-ceb9145f307d", "name": "Competitor Research" }] }],
+ "phases": [{ "id": "cbad8cd9-5696-4f00-b463-a814e5a1c984", "color_code": "#a9a9a9", "name": "Design" }, { "id": "d947e01c-f7d6-4bc2-80d2-f272e9f5a3f5", "color_code": "#a9a9a9", "name": "Production" }, { "id": "c97faab3-5606-4dd3-8f05-42eb6cfedc71", "color_code": "#a9a9a9", "name": "Research" }, { "id": "d84fa4de-d54c-45d7-aba0-9ee34c1348db", "color_code": "#a9a9a9", "name": "Concepts" }],
+ "status": [{ "id": "18b223ba-26ff-49ff-aad4-296d535a9b10", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "b47baaa8-28b1-4559-b619-55994a3a8d4a", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "f1e52628-efff-4280-bba9-a19f5c14cec1", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "0253619d-f73b-4d31-9c6b-b2e78753bbb9",
+ "name": "Education",
+ "key": "EDU",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "b8710df5-64c1-468a-ae21-8f9eb1246568", "name": "Class Scheduling", "description": null, "total_minutes": 0, "sort_order": 8, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 8, "parent_task_id": null, "status_id": "1ffb2c8d-611c-44b3-b983-dd6b0aa37a8b", "status_name": "Done", "phase_id": "c895cee8-ce95-489e-a245-146eafc4a84c", "phase_name": "Discovery", "labels": [{ "label_id": "cda17dc3-8ced-4026-8c15-6b8a0f1509b5", "name": "Subject" }] }, { "id": "0905cbc1-6991-4bcd-99e6-f7655e88d382", "name": "Curriculum Development", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "cbc9034f-eec0-4779-84b9-854b1e2f272d", "status_name": "Doing", "phase_id": "8058525a-0797-45c0-b7fb-e7cac00fa384", "phase_name": "Closure", "labels": [{ "label_id": "a6c5c579-f6a1-469d-9945-5e130e761263", "name": "Semester" }] }, { "id": "f5260d22-3fcb-4065-b5ee-1db947bf65bf", "name": "Assignments and Homework", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "cbc9034f-eec0-4779-84b9-854b1e2f272d", "status_name": "Doing", "phase_id": "c895cee8-ce95-489e-a245-146eafc4a84c", "phase_name": "Discovery", "labels": [{ "label_id": "48be7a1c-5253-435d-9805-8bfa9d89e401", "name": "Assignment Type" }] }, { "id": "e6d2c8d2-848a-4072-bbe9-f19b10e49ec5", "name": "Grading and Assessment", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "1ffb2c8d-611c-44b3-b983-dd6b0aa37a8b", "status_name": "Done", "phase_id": "8058525a-0797-45c0-b7fb-e7cac00fa384", "phase_name": "Closure", "labels": [{ "label_id": "15057ddf-b633-4949-9f66-14640715f8bb", "name": "Professional Development" }] }, { "id": "f8ccdec7-65f8-47d7-ab80-44ceaca9b5a0", "name": "Student Progress Tracking", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "cbc9034f-eec0-4779-84b9-854b1e2f272d", "status_name": "Doing", "phase_id": "505f4682-2d0d-4c96-8f8c-be6cd8038d4c", "phase_name": "Execute", "labels": [{ "label_id": "c2352891-7a7f-4907-a911-a6870721a563", "name": "Course Material" }] }, { "id": "87f57f84-2e10-4915-96c8-f6dd0f190be7", "name": "Resource Management", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "3d16abb5-e17d-4e7a-88f5-2df32d9e2799", "status_name": "To Do", "phase_id": "505f4682-2d0d-4c96-8f8c-be6cd8038d4c", "phase_name": "Execute", "labels": [{ "label_id": "c39fe4d6-6732-4e94-8126-bdac0a2e8d93", "name": "Special Programs" }] }, { "id": "141fc32d-aeaa-4ace-b852-071a754c5e7c", "name": "Research Projects", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "3d16abb5-e17d-4e7a-88f5-2df32d9e2799", "status_name": "To Do", "phase_id": "d44679f5-ebc1-4fb9-82ef-7967fc1a4355", "phase_name": "Initiation", "labels": [{ "label_id": "ca5a5419-249a-4d5a-b20e-c40dda0898b3", "name": "Event type" }] }, { "id": "d68c8af1-6936-412b-9945-f28d6772d7ee", "name": "Event Planning", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "3d16abb5-e17d-4e7a-88f5-2df32d9e2799", "status_name": "To Do", "phase_id": "d44679f5-ebc1-4fb9-82ef-7967fc1a4355", "phase_name": "Initiation", "labels": [{ "label_id": "1f716400-0189-4ef0-9e75-3140d2425f03", "name": "Technology Integration" }] }, { "id": "c6bc044e-08d0-456b-96b6-01b026f94bc6", "name": "Budget Management", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "3d16abb5-e17d-4e7a-88f5-2df32d9e2799", "status_name": "To Do", "phase_id": "5888995b-2efe-4a5e-aa07-28c6008dbd4a", "phase_name": "Planning", "labels": [{ "label_id": "a8586373-d27c-44c1-a195-32c68ad6c35b", "name": "Department" }] }, { "id": "7bb51913-812a-4d18-81a4-d8561b47f892", "name": "Online Learning Management", "description": null, "total_minutes": 0, "sort_order": 9, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 9, "parent_task_id": null, "status_id": "1ffb2c8d-611c-44b3-b983-dd6b0aa37a8b", "status_name": "Done", "phase_id": "5888995b-2efe-4a5e-aa07-28c6008dbd4a", "phase_name": "Planning", "labels": [{ "label_id": "f40d2ce4-e624-4ae6-8ab3-2ee6313d9859", "name": "Resource Type" }] }],
+ "phases": [{ "id": "c895cee8-ce95-489e-a245-146eafc4a84c", "color_code": "#a9a9a9", "name": "Discovery" }, { "id": "505f4682-2d0d-4c96-8f8c-be6cd8038d4c", "color_code": "#a9a9a9", "name": "Execute" }, { "id": "d44679f5-ebc1-4fb9-82ef-7967fc1a4355", "color_code": "#a9a9a9", "name": "Initiation" }, { "id": "5888995b-2efe-4a5e-aa07-28c6008dbd4a", "color_code": "#a9a9a9", "name": "Planning" }, { "id": "8058525a-0797-45c0-b7fb-e7cac00fa384", "color_code": "#a9a9a9", "name": "Closure" }],
+ "status": [{ "id": "3d16abb5-e17d-4e7a-88f5-2df32d9e2799", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "cbc9034f-eec0-4779-84b9-854b1e2f272d", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "1ffb2c8d-611c-44b3-b983-dd6b0aa37a8b", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "faca59ab-9e35-4453-89e6-3428415ee8f1",
+ "name": "Finance",
+ "key": "FIN",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "bbf58010-cf84-464d-a3b4-ebaf3c260995", "name": "Budget Planning", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "ad5faad3-8fbb-4436-ae29-9876907e9383", "phase_name": "Develop Financial Goals", "labels": [{ "label_id": "8cb78e68-1202-460e-80f3-ed1576cceba7", "name": "Personnel Costs" }, { "label_id": "9afd37ed-bda5-4667-ae97-0f04454cd117", "name": "Travel Expenses" }] }, { "id": "b737b4ab-fb08-4fee-a11f-2428d82e4540", "name": "Financial Reporting", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "af80344f-0295-4957-9a05-67225641ba36", "phase_name": "Identify Alternative Actions", "labels": [{ "label_id": "3f077d55-5d21-432b-ba8f-ac4a032c1abb", "name": "Income Statements" }, { "label_id": "dbf839a0-ecc8-4b7a-a95c-9fe00d0025d5", "name": "Balance Sheets" }] }, { "id": "fd992186-50e0-4247-918f-e191a0979e8b", "name": "Vendor and Supplier Management", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "085b7679-c541-4f0f-a3c3-4f74549a01b8", "phase_name": "Implementation of Action Plan", "labels": [{ "label_id": "e1c2f730-1666-4c47-ab5c-0ee4a4483d56", "name": "Audit in Progress" }] }, { "id": "52d5ca73-f0c7-41a5-8183-b978a7e29bc0", "name": "Invoice Management", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "085b7679-c541-4f0f-a3c3-4f74549a01b8", "phase_name": "Implementation of Action Plan", "labels": [{ "label_id": "3f077d55-5d21-432b-ba8f-ac4a032c1abb", "name": "Income Statements" }] }, { "id": "473c2f5c-5cec-45f5-ad48-d608696b7380", "name": "Expense Approval Workflow", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "efe47201-fe2b-4812-aced-1ed1e4d2d34c", "phase_name": "Review", "labels": [{ "label_id": "9afd37ed-bda5-4667-ae97-0f04454cd117", "name": "Travel Expenses" }] }, { "id": "8c5cfb63-f5e0-4bf5-961b-1fbeb8be49df", "name": "Tax Planning and Compliance", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "efe47201-fe2b-4812-aced-1ed1e4d2d34c", "phase_name": "Review", "labels": [{ "label_id": "07aa2bb7-e884-4fd4-90ec-08feffaf936c", "name": "Income Tax" }, { "label_id": "5210970e-7343-448d-a4a3-cd91ff7b73ab", "name": "Sales Tax" }] }, { "id": "f049a974-643f-4bb4-bf1c-4e896daf1ba1", "name": "Budget Monitoring", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "af80344f-0295-4957-9a05-67225641ba36", "phase_name": "Identify Alternative Actions", "labels": [{ "label_id": "4910e41f-19f5-49be-89d5-6dd17de2a89f", "name": "Equipment Costs" }] }, { "id": "8500d90e-0f92-46b6-83fd-281c1653d301", "name": "Expense Tracking", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "status_name": "To Do", "phase_id": "af80344f-0295-4957-9a05-67225641ba36", "phase_name": "Identify Alternative Actions", "labels": [{ "label_id": "0c3857fd-ce14-4611-9e38-c932386e09e5", "name": "Capital Expenses" }] }],
+ "phases": [{ "id": "af80344f-0295-4957-9a05-67225641ba36", "color_code": "#a9a9a9", "name": "Identify Alternative Actions" }, { "id": "085b7679-c541-4f0f-a3c3-4f74549a01b8", "color_code": "#a9a9a9", "name": "Implementation of Action Plan" }, { "id": "efe47201-fe2b-4812-aced-1ed1e4d2d34c", "color_code": "#a9a9a9", "name": "Review" }, { "id": "ad5faad3-8fbb-4436-ae29-9876907e9383", "color_code": "#a9a9a9", "name": "Develop Financial Goals" }],
+ "status": [{ "id": "dc51c9f2-9164-4eea-98eb-e96d8e1f18e4", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "a84f791b-0962-43e2-b987-3f67d57a5c00", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "fbffebcb-6c91-4209-a6c3-6382ac821db7", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "95984b31-7122-4771-b65c-51fef9492727",
+ "name": "HR & Recruiting",
+ "key": "HR",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "be816010-11b3-4dfb-8ab6-a55ed05bf851", "name": "Identify and reach out to potential candidates through various channels", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "c53db618-e1f7-48d4-8e5a-5715821bdc24", "status_name": "To Do", "phase_id": "4863b6e2-c54e-4fdc-92c5-651b3a71a685", "phase_name": "Initiation", "labels": [{ "label_id": "0931cc0a-4753-49b9-a2e3-84a544a71075", "name": "Interviewing" }] }, { "id": "911f58a7-c903-4279-870d-99f922d34c34", "name": "Define the job position, responsibilities, and requirements.", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "c53db618-e1f7-48d4-8e5a-5715821bdc24", "status_name": "To Do", "phase_id": "6a0373cf-8b6f-4b93-8819-58d19bf3a05e", "phase_name": "Control", "labels": [{ "label_id": "6447851e-2f9b-4eeb-9659-24dbfbbb8b18", "name": "Screening" }] }, { "id": "fba04ea8-55f2-4b1b-a317-8665898347d3", "name": "Schedule and conduct interviews with shortlisted candidates.", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "083ed353-31e1-4a6c-8109-0ada5633d77c", "status_name": "Doing", "phase_id": "78141332-9fd1-4fb3-a44d-c45f4d0aff64", "phase_name": "Close", "labels": [{ "label_id": "093426db-8e2c-4238-8588-50b5cc685b7a", "name": "Assessment" }] }, { "id": "67aaa1aa-e586-48c8-9d0b-0d5a63e291b5", "name": "Contact provided references to validate candidates' background and skills.", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "083ed353-31e1-4a6c-8109-0ada5633d77c", "status_name": "Doing", "phase_id": "b1b2d1ea-8030-4a86-b217-0a6cdad7342b", "phase_name": "Execution", "labels": [{ "label_id": "ce7feb2b-0416-417b-a07c-dabb16237cb8", "name": "Onboarding" }] }, { "id": "9e4dac02-5431-40dd-bd20-2a5bc1d1a21f", "name": "Collect feedback from interviewers and team members", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "d5b9c357-9f48-45be-8b0a-e7391e99e2dd", "status_name": "Done", "phase_id": "78141332-9fd1-4fb3-a44d-c45f4d0aff64", "phase_name": "Close", "labels": [{ "label_id": "492cfb8b-05d5-48f3-a07e-4586eda0c8b5", "name": "Rejected" }] }, { "id": "71bb1794-3836-48df-ac06-f8702bbd5f54", "name": "Document feedback for future reference and improvement.", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "d5b9c357-9f48-45be-8b0a-e7391e99e2dd", "status_name": "Done", "phase_id": "9b619988-87a8-4b70-abd6-54acc6222d3f", "phase_name": "Planning", "labels": [{ "label_id": "a330cea8-4095-4911-90b1-8c7df5382104", "name": "Pending" }] }],
+ "phases": [{ "id": "78141332-9fd1-4fb3-a44d-c45f4d0aff64", "color_code": "#a9a9a9", "name": "Close" }, { "id": "6a0373cf-8b6f-4b93-8819-58d19bf3a05e", "color_code": "#a9a9a9", "name": "Control" }, { "id": "b1b2d1ea-8030-4a86-b217-0a6cdad7342b", "color_code": "#a9a9a9", "name": "Execution" }, { "id": "4863b6e2-c54e-4fdc-92c5-651b3a71a685", "color_code": "#a9a9a9", "name": "Initiation" }, { "id": "9b619988-87a8-4b70-abd6-54acc6222d3f", "color_code": "#a9a9a9", "name": "Planning" }],
+ "status": [{ "id": "c53db618-e1f7-48d4-8e5a-5715821bdc24", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "083ed353-31e1-4a6c-8109-0ada5633d77c", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "d5b9c357-9f48-45be-8b0a-e7391e99e2dd", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "d249f484-7813-47b7-845d-1f1640d9b4a9",
+ "name": "Information Technology",
+ "key": "IT",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "2c0d4fc1-e04c-4175-8dff-6ff4bf8e656c", "name": "Designing, coding, testing, and maintaining software applications and systems.", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "a2410c64-7d98-427f-8a57-13f38b11a62e", "status_name": "To Do", "phase_id": "06715231-cf91-4e2d-8888-ee86db854b0f", "phase_name": "Design", "labels": [{ "label_id": "40a8a57e-7f0f-4311-ac88-2e2ccc5cce87", "name": "User Stories" }] }, { "id": "c048b765-002c-4e39-a29e-1683226b1ce0", "name": "Managing servers and computer systems, including installation, configuration, updates, and maintenance.", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "9b965f47-486c-4411-9e50-bca4375e1d0e", "status_name": "Doing", "phase_id": "ebbf9968-f568-425b-a306-89154f007224", "phase_name": "Development", "labels": [{ "label_id": "66f9fdd1-81d9-4b84-9d45-deef14b3a5e1", "name": "Reporting and Analytics" }] }, { "id": "4c17d53c-ff43-4e9c-8940-2aff0e1a8692", "name": "Conducting security audits, vulnerability assessments, and risk management.", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "8af8b1c1-5642-4b7b-a67e-73030d082810", "status_name": "Done", "phase_id": "38247a43-cd89-434c-9572-b14036ff1368", "phase_name": "Execution", "labels": [{ "label_id": "2c2f10a3-6b87-480d-9433-234999b5cbed", "name": "Backlog" }] }, { "id": "1e8a20bd-1304-49d7-9144-7346cebc1ec8", "name": "Managing data integrity, security, and backups.", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "a2410c64-7d98-427f-8a57-13f38b11a62e", "status_name": "To Do", "phase_id": "bd1e3b78-a32a-4661-8b14-d47b9d29bdb6", "phase_name": "Strategy", "labels": [{ "label_id": "de3465e1-f773-4396-b0fe-f5aba28949b9", "name": "Risk Management" }] }, { "id": "85a004ac-e5c3-41d2-8763-bc82c02173a7", "name": "Performance Optimization", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "a2410c64-7d98-427f-8a57-13f38b11a62e", "status_name": "To Do", "phase_id": "ebbf9968-f568-425b-a306-89154f007224", "phase_name": "Development", "labels": [{ "label_id": "8a5dd526-6041-4ff8-834e-452595817515", "name": "Optimization" }] }],
+ "phases": [{ "id": "ebbf9968-f568-425b-a306-89154f007224", "color_code": "#a9a9a9", "name": "Development" }, { "id": "38247a43-cd89-434c-9572-b14036ff1368", "color_code": "#a9a9a9", "name": "Execution" }, { "id": "bd1e3b78-a32a-4661-8b14-d47b9d29bdb6", "color_code": "#a9a9a9", "name": "Strategy" }, { "id": "06715231-cf91-4e2d-8888-ee86db854b0f", "color_code": "#a9a9a9", "name": "Design" }],
+ "status": [{ "id": "a2410c64-7d98-427f-8a57-13f38b11a62e", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "9b965f47-486c-4411-9e50-bca4375e1d0e", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "8af8b1c1-5642-4b7b-a67e-73030d082810", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "916c4517-7779-4fad-8a0c-ea21fb6c5640",
+ "name": "Legal",
+ "key": "LEG",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "1111f3cc-d90a-4cbe-970e-2f58b626d9cd", "name": "Privacy and Data Protection", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "92b9ca5b-6736-4db3-8fdb-73ff690986b7", "status_name": "Doing", "phase_id": "448f5562-8d80-43eb-885b-94ba692e316f", "phase_name": "Deliver", "labels": [{ "label_id": "1cd869cc-f440-4746-9386-3e55585ab92d", "name": "Document Review" }] }, { "id": "0a360685-3c04-4df8-a5e9-5290cba7d1c4", "name": "Regulatory Filings", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "4c267b12-0c5e-4922-ac4d-e310584e3a9c", "status_name": "Done", "phase_id": "f255eeb2-f12f-40e7-b01a-d818fef25753", "phase_name": "Define", "labels": [{ "label_id": "5449bb89-192b-4786-a281-9d6857ae0d8a", "name": "Depositions" }] }, { "id": "234dcf0f-d791-4476-ad3c-cb4405981ee9", "name": "Litigation and Dispute Resolution", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "4c267b12-0c5e-4922-ac4d-e310584e3a9c", "status_name": "Done", "phase_id": "810a356d-d875-4606-9915-7a00f86a1415", "phase_name": "Close", "labels": [{ "label_id": "d682059e-a8e7-4235-9113-6eee72ca9b3f", "name": "Mediation" }] }, { "id": "a07cac11-97a9-45fe-bd94-f0623dc7cde6", "name": "Contract Management", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "e8b1c4f4-fed3-4380-b4c0-d014f995f5cd", "status_name": "To Do", "phase_id": "448f5562-8d80-43eb-885b-94ba692e316f", "phase_name": "Deliver", "labels": [{ "label_id": "bb4becb6-5b8f-4877-8426-e14980fca5a5", "name": "Contracts" }] }, { "id": "52a2208f-cbb3-40a7-a1c5-851af10fafa8", "name": "Contract Renewals and Expirations", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "e8b1c4f4-fed3-4380-b4c0-d014f995f5cd", "status_name": "To Do", "phase_id": "2bb33ac2-e1c0-428d-bf09-86fc8c4608e4", "phase_name": "Plan", "labels": [{ "label_id": "715962d3-57a3-46c2-84f3-31cafdd0cc63", "name": "Open" }] }, { "id": "9ddc280d-58a2-489e-bfd3-fd6f151d4575", "name": "Compliance Checks", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "92b9ca5b-6736-4db3-8fdb-73ff690986b7", "status_name": "Doing", "phase_id": "810a356d-d875-4606-9915-7a00f86a1415", "phase_name": "Close", "labels": [{ "label_id": "4d3def84-dc8b-4808-ac44-6fef85524bb8", "name": "Court Filings" }] }, { "id": "5265ee29-6a6b-4478-97ae-3bc45f5e006c", "name": "Legal Research", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "e8b1c4f4-fed3-4380-b4c0-d014f995f5cd", "status_name": "To Do", "phase_id": "2bb33ac2-e1c0-428d-bf09-86fc8c4608e4", "phase_name": "Plan", "labels": [{ "label_id": "dae4a050-c33d-40ef-9140-0af77f7f0110", "name": "Closed" }] }, { "id": "bd6f0b77-685b-476b-b6c4-7c60a070b8c7", "name": "Intellectual Property Management", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "92b9ca5b-6736-4db3-8fdb-73ff690986b7", "status_name": "Doing", "phase_id": "810a356d-d875-4606-9915-7a00f86a1415", "phase_name": "Close", "labels": [{ "label_id": "7137e9c0-17f3-4f22-9e77-b767ea461f2e", "name": "Legal Research" }] }],
+ "phases": [{ "id": "810a356d-d875-4606-9915-7a00f86a1415", "color_code": "#a9a9a9", "name": "Close" }, { "id": "f255eeb2-f12f-40e7-b01a-d818fef25753", "color_code": "#a9a9a9", "name": "Define" }, { "id": "2bb33ac2-e1c0-428d-bf09-86fc8c4608e4", "color_code": "#a9a9a9", "name": "Plan" }, { "id": "448f5562-8d80-43eb-885b-94ba692e316f", "color_code": "#a9a9a9", "name": "Deliver" }],
+ "status": [{ "id": "e8b1c4f4-fed3-4380-b4c0-d014f995f5cd", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "92b9ca5b-6736-4db3-8fdb-73ff690986b7", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "4c267b12-0c5e-4922-ac4d-e310584e3a9c", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "0fffc630-28d8-4cd3-8302-19d4bc846c57",
+ "name": "Manufacturing",
+ "key": "MAN",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "59243c3e-5e5f-4049-bb84-06302cccb67a", "name": "Develop detailed product design specifications, including materials, dimensions, and functionality.", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "146fcff7-3a01-4445-ae39-ac28586edcb0", "phase_name": "Design", "labels": [{ "label_id": "305ce3cc-026a-48a9-8191-348c60a76f50", "name": "Product Design" }] }, { "id": "91a4a0cb-d163-4548-90f2-be1c1e65504e", "name": "Collaborate with the design and engineering teams to brainstorm innovative ideas for product features and improvements.", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "1c717dd2-056a-4e06-aa44-03f607ffc573", "phase_name": "Ideation", "labels": [{ "label_id": "e0ab7a83-3f20-48c4-bdca-85c4fbb1fcec", "name": "Brainstorming" }] }, { "id": "c6a8852b-2a8a-43ad-b268-49f7ff2abddf", "name": "Conduct market research to assess the viability of proposed product enhancements and innovations.", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "146fcff7-3a01-4445-ae39-ac28586edcb0", "phase_name": "Design", "labels": [{ "label_id": "bc877c9e-44a2-45a4-8fd0-c55a68d0bd1f", "name": "Market Research" }] }, { "id": "a8e05dd8-091c-4ae5-ab8b-708433f20f08", "name": "Develop detailed manufacturing processes, including workflow, quality control measures, and production schedules.", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "6643cda8-0f0e-4940-a386-64c166f99234", "phase_name": "Pre-production", "labels": [{ "label_id": "41e0e56d-a63c-4358-8058-ba4b61bd71cf", "name": "Process Planning" }] }, { "id": "41f5d964-96d1-431e-8d23-e23b2383cba8", "name": "Plan and optimize the supply chain, including sourcing raw materials, logistics, and inventory management.", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "6643cda8-0f0e-4940-a386-64c166f99234", "phase_name": "Pre-production", "labels": [{ "label_id": "9b264c91-4b0f-4f10-b089-3a10687f6e89", "name": "Supply Chain Planning" }] }, { "id": "63fe093d-1751-47de-8f6e-935ee4f5e1a4", "name": "Create prototypes of the product to validate the design and manufacturing processes", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "59d4621a-21c7-4abd-87cf-a482996bda38", "phase_name": "Prototyping", "labels": [{ "label_id": "a5ff7d41-af9b-4b6a-b095-1737156d6a0e", "name": "Prototype Development" }] }, { "id": "b84a1604-7343-4dcd-9a06-a89d49d1037a", "name": "Create designs for the molds, tooling, and equipment needed in the manufacturing process.", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "146fcff7-3a01-4445-ae39-ac28586edcb0", "phase_name": "Design", "labels": [{ "label_id": "754cb86b-2d59-4d4b-940a-f4dc8cf420f0", "name": "Tooling Design" }] }, { "id": "defb53e7-82be-49d2-89f4-0e5e02c9cc5b", "name": "Perform rigorous testing and validation on prototypes to ensure they meet quality and performance standards.", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "status_name": "To Do", "phase_id": "59d4621a-21c7-4abd-87cf-a482996bda38", "phase_name": "Prototyping", "labels": [{ "label_id": "80fffaff-6317-4adf-b08f-c1b946329dca", "name": "Testing and Validation" }] }],
+ "phases": [{ "id": "1c717dd2-056a-4e06-aa44-03f607ffc573", "color_code": "#a9a9a9", "name": "Ideation" }, { "id": "146fcff7-3a01-4445-ae39-ac28586edcb0", "color_code": "#a9a9a9", "name": "Design" }, { "id": "59d4621a-21c7-4abd-87cf-a482996bda38", "color_code": "#a9a9a9", "name": "Prototyping" }, { "id": "6643cda8-0f0e-4940-a386-64c166f99234", "color_code": "#a9a9a9", "name": "Pre-production" }],
+ "status": [{ "id": "83e13fbb-41df-4ab1-b44b-97f0cceea227", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "747d2714-f83d-4559-ae21-58462422faf3", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "c8cd1467-b159-454f-be15-457443f6338c", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "2eb7b496-8b43-4678-8b03-cbd036e3d310",
+ "name": "Marketing",
+ "key": "MAR",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "4fc3ada1-75e7-4fc6-b592-c6d3888fc1ec", "name": "Delivering value to your customers and leads", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "c8ab0d1c-5018-47f1-910a-cbd1f7db08c6", "status_name": "To Do", "phase_id": "0a9e1217-18ee-4b98-8b6b-d0a7fc14d4dc", "phase_name": "Client Review", "labels": [{ "label_id": "ca9e18a4-13d3-427e-b1be-5e29ab0d1e8b", "name": "Email marketing" }] }, { "id": "14e07998-b6bf-4b90-ac00-c89114212cbd", "name": "Introducing new products or services", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "c573e94b-bc06-4596-acdf-8df2c5bf6592", "status_name": "Doing", "phase_id": "246d8070-8041-45ac-a8b3-1b0558030ef3", "phase_name": "In progress", "labels": [{ "label_id": "aa29fe85-55a9-43de-a941-0026141b94e4", "name": "Campaign management" }] }, { "id": "501fc74b-5e88-404a-8935-6f1ca11855d2", "name": "Collecting feedback from customers", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "919feb50-a565-479c-bb7d-21c505eb14ea", "status_name": "Done", "phase_id": "65c80bf8-e19f-427c-b514-62ee0d7ef93e", "phase_name": "Unstarted", "labels": [{ "label_id": "f3d382ea-c8c1-4450-b3f4-ee96cc831e1c", "name": "Social media" }] }, { "id": "f8e103f0-929a-4ec5-94a4-d0967fed871e", "name": "Building marketing strategies and campaigns", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "919feb50-a565-479c-bb7d-21c505eb14ea", "status_name": "Done", "phase_id": "65c80bf8-e19f-427c-b514-62ee0d7ef93e", "phase_name": "Unstarted", "labels": [{ "label_id": "c67325ea-731c-491b-9c65-0f27b02c529c", "name": "Marketing operations" }] }, { "id": "573eff9d-fa59-4456-b8c0-34421fa29611", "name": "Tracking and monitoring marketing campaigns", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "919feb50-a565-479c-bb7d-21c505eb14ea", "status_name": "Done", "phase_id": "246d8070-8041-45ac-a8b3-1b0558030ef3", "phase_name": "In progress", "labels": [{ "label_id": "6bb59836-eff0-4bba-b887-87c33dab57c1", "name": "Event marketing" }] }, { "id": "b64172a0-cf88-4f7b-921c-ef4d48b1f8c7", "name": "Creating a strong and dependable brand", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "c573e94b-bc06-4596-acdf-8df2c5bf6592", "status_name": "Doing", "phase_id": "a4340ee9-8b5d-42d5-b7c4-33c23664c539", "phase_name": "Completed", "labels": [{ "label_id": "c67325ea-731c-491b-9c65-0f27b02c529c", "name": "Marketing operations" }] }, { "id": "61f508a9-ca41-408c-a7e8-b57a6648287d", "name": "Boosting company sales", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "c8ab0d1c-5018-47f1-910a-cbd1f7db08c6", "status_name": "To Do", "phase_id": "0a9e1217-18ee-4b98-8b6b-d0a7fc14d4dc", "phase_name": "Client Review", "labels": [{ "label_id": "d5370275-f844-4138-b357-baf66d909fae", "name": "Task management" }] }],
+ "phases": [{ "id": "a4340ee9-8b5d-42d5-b7c4-33c23664c539", "color_code": "#a9a9a9", "name": "Completed" }, { "id": "246d8070-8041-45ac-a8b3-1b0558030ef3", "color_code": "#a9a9a9", "name": "In progress" }, { "id": "65c80bf8-e19f-427c-b514-62ee0d7ef93e", "color_code": "#a9a9a9", "name": "Unstarted" }, { "id": "0a9e1217-18ee-4b98-8b6b-d0a7fc14d4dc", "color_code": "#a9a9a9", "name": "Client Review" }],
+ "status": [{ "id": "c8ab0d1c-5018-47f1-910a-cbd1f7db08c6", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "c573e94b-bc06-4596-acdf-8df2c5bf6592", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "919feb50-a565-479c-bb7d-21c505eb14ea", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "2a2b87f8-4e07-4385-bb90-b6140a924dad",
+ "name": "Nonprofit",
+ "key": "NON",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "870fd443-cdcf-4f37-95e7-29c3d2af4b06", "name": "Resource Management", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "56677096-7e06-4f0f-b305-c25cd0acae32", "status_name": "Doing", "phase_id": "c33e08d1-8434-4be9-b2c6-1c440dd528c3", "phase_name": "Planning", "labels": [{ "label_id": "4fd02e4f-5683-4f82-a57b-7766597d3ad6", "name": "Volunteer Recruitment" }] }, { "id": "9de8c9d2-6b11-4282-a99b-a0afc0b8a474", "name": "Budgeting and Fundraising", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "56677096-7e06-4f0f-b305-c25cd0acae32", "status_name": "Doing", "phase_id": "d82a1df4-2ed3-444a-ae75-33109d906509", "phase_name": "Excuation", "labels": [{ "label_id": "a83dd9fc-bd67-4748-bd68-eeb87fd01dcc", "name": "Program Development" }] }, { "id": "21d655d9-1cec-4069-9980-55b5979d6045", "name": "Project Planning", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "4836992e-043e-4d13-898a-c448567a6630", "status_name": "To Do", "phase_id": "d82a1df4-2ed3-444a-ae75-33109d906509", "phase_name": "Excuation", "labels": [{ "label_id": "30a9d9e6-fb49-4552-a5ec-e14ab75198ea", "name": "Event Planning" }] }, { "id": "41ca73eb-fa65-4cb8-a22d-a339f102a675", "name": "Task Management", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "3b0182a8-ec59-4732-a76c-0e951e8605fa", "status_name": "Done", "phase_id": "c9974233-f781-4265-87a1-0fdfcd4dd97d", "phase_name": "Implementation", "labels": [{ "label_id": "f95ed012-5259-48ca-a679-dad1203a3371", "name": "Volunteer Orientation" }] }, { "id": "d4ea081c-a819-4011-bdcd-d2d7707e288e", "name": "Project Initiation", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "4836992e-043e-4d13-898a-c448567a6630", "status_name": "To Do", "phase_id": "bf390b1f-10b7-4969-887d-ddeffc9a8aa8", "phase_name": "Close", "labels": [{ "label_id": "025dd865-b983-4254-9599-aaff0a19bbfa", "name": "Environmental" }] }, { "id": "254bbe51-1d91-48cb-ab9a-d9b779c83e3d", "name": "Communication and Collaboration", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "3b0182a8-ec59-4732-a76c-0e951e8605fa", "status_name": "Done", "phase_id": "d82a1df4-2ed3-444a-ae75-33109d906509", "phase_name": "Excuation", "labels": [{ "label_id": "61bac8dd-901f-4859-9a51-74d059deea11", "name": "Impact Assessment" }] }],
+ "phases": [{ "id": "d82a1df4-2ed3-444a-ae75-33109d906509", "color_code": "#a9a9a9", "name": "Excuation" }, { "id": "c9974233-f781-4265-87a1-0fdfcd4dd97d", "color_code": "#a9a9a9", "name": "Implementation" }, { "id": "bf390b1f-10b7-4969-887d-ddeffc9a8aa8", "color_code": "#a9a9a9", "name": "Close" }, { "id": "c33e08d1-8434-4be9-b2c6-1c440dd528c3", "color_code": "#a9a9a9", "name": "Planning" }],
+ "status": [{ "id": "4836992e-043e-4d13-898a-c448567a6630", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "56677096-7e06-4f0f-b305-c25cd0acae32", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "3b0182a8-ec59-4732-a76c-0e951e8605fa", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "9d95eb0f-6771-4e78-a643-a9222691efa5",
+ "name": "Personal use",
+ "key": "PU",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "46d2249b-8322-41c7-b1c4-486143247cfb", "name": "Home Renovation Project", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "9ed3975d-4c6b-44b5-ad06-31ac804ba2ae", "status_name": "To Do", "phase_id": "e9df4cbb-32a8-4556-b680-ce45a8a4a4a5", "phase_name": "Completed", "labels": [{ "label_id": "50b2a3d6-c23d-403e-ab3d-2a8e24f64a92", "name": "Health and Fitness" }] }, { "id": "5b06f6b4-61d1-414e-b7f2-f5efa7a993ba", "name": "Education and Self-Improvement", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "9ed3975d-4c6b-44b5-ad06-31ac804ba2ae", "status_name": "To Do", "phase_id": "e9df4cbb-32a8-4556-b680-ce45a8a4a4a5", "phase_name": "Completed", "labels": [{ "label_id": "c761f1e1-9a47-4769-a82c-98f18c261de2", "name": "Errands" }] }, { "id": "b8b01343-37e1-4ace-bb74-252826bac091", "name": "Event Planning", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "9ed3975d-4c6b-44b5-ad06-31ac804ba2ae", "status_name": "To Do", "phase_id": "c1b23b8d-4583-442b-8e59-2b0613da2fd3", "phase_name": "Planning", "labels": [{ "label_id": "24bb4417-b60f-4a60-a0c0-71113af0c428", "name": "Collaborative" }] }, { "id": "ee453484-8960-42bc-af61-25b2e2b8cb90", "name": "Fitness and Health Goals", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "b3de861e-c883-4983-92ed-e89613009ebb", "status_name": "Doing", "phase_id": "8f1ed0de-92eb-4286-8d3b-a1270868a4e2", "phase_name": "Execute", "labels": [{ "label_id": "17657792-a0d7-40ff-9eee-774866cc3164", "name": "Uninspired" }] }, { "id": "9a37665c-40ec-4715-ad79-47dc2a188cdd", "name": "Vacation Planning", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "b3de861e-c883-4983-92ed-e89613009ebb", "status_name": "Doing", "phase_id": "8f1ed0de-92eb-4286-8d3b-a1270868a4e2", "phase_name": "Execute", "labels": [{ "label_id": "995e4d3a-98ad-48e6-a24c-3ddc4f0bdea4", "name": "Treat Yourself" }] }, { "id": "a920fe42-9f1e-4362-80ef-24d2bc8306bb", "name": "Home Organization", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "ee7f06a9-4b9e-4277-8caf-d3ed8360c9f7", "status_name": "Done", "phase_id": "7982091e-00ba-47b6-b404-0296de71ca7b", "phase_name": "Not started", "labels": [{ "label_id": "722eb3ca-fd06-449d-b7a7-e7c010ac393a", "name": "Home Improvement" }] }, { "id": "3247406f-5037-45cb-ab12-ab07a14df594", "name": "Personal Blog", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "ee7f06a9-4b9e-4277-8caf-d3ed8360c9f7", "status_name": "Done", "phase_id": "c1b23b8d-4583-442b-8e59-2b0613da2fd3", "phase_name": "Planning", "labels": [{ "label_id": "67d47114-247d-4b73-9251-420993931411", "name": "Education" }] }, { "id": "a2998d92-356b-4eb4-b936-cde2d3e6511a", "name": "Financial Goals", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "ee7f06a9-4b9e-4277-8caf-d3ed8360c9f7", "status_name": "Done", "phase_id": "7982091e-00ba-47b6-b404-0296de71ca7b", "phase_name": "Not started", "labels": [{ "label_id": "9afd37ed-bda5-4667-ae97-0f04454cd117", "name": "Travel Expenses" }] }],
+ "phases": [{ "id": "e9df4cbb-32a8-4556-b680-ce45a8a4a4a5", "color_code": "#a9a9a9", "name": "Completed" }, { "id": "8f1ed0de-92eb-4286-8d3b-a1270868a4e2", "color_code": "#a9a9a9", "name": "Execute" }, { "id": "c1b23b8d-4583-442b-8e59-2b0613da2fd3", "color_code": "#a9a9a9", "name": "Planning" }, { "id": "7982091e-00ba-47b6-b404-0296de71ca7b", "color_code": "#a9a9a9", "name": "Not started" }],
+ "status": [{ "id": "9ed3975d-4c6b-44b5-ad06-31ac804ba2ae", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "b3de861e-c883-4983-92ed-e89613009ebb", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "ee7f06a9-4b9e-4277-8caf-d3ed8360c9f7", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "d40758fc-9fb0-49fb-9ad1-42a01d85c2d5",
+ "name": "Sales & CRM",
+ "key": "SC",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "db31fee2-2a6f-4867-b152-4e98826ba96e", "name": "Integration with Existing Systems", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "2dc0474f-5cac-4112-a904-fcbc109621e4", "status_name": "Done", "phase_id": "8b6e69af-82eb-4aeb-897f-918df3d108c3", "phase_name": "Opportunity", "labels": [{ "label_id": "c91136fd-fffe-4b1b-9a83-643e06db1731", "name": "User Training" }] }, { "id": "70272627-fa50-4bba-b9df-c6f9dcc1ab6b", "name": "Initial Contact and Relationship Building", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "ac628ff7-6fdc-4371-b0f6-5a093b9c50cf", "status_name": "Doing", "phase_id": "8b6e69af-82eb-4aeb-897f-918df3d108c3", "phase_name": "Opportunity", "labels": [{ "label_id": "53d0cf0d-03b4-43c2-94ea-5c6547de7d9e", "name": "Data Migration" }] }, { "id": "cf33280a-80de-44c2-ac06-a0d7030250e2", "name": "Lead Qualification and Research", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "a7604c1f-9d75-46d7-82cf-42f2c27c79d7", "status_name": "To Do", "phase_id": "2f135803-6ee4-4dd5-a5e5-65d630fa2f6a", "phase_name": "Lead", "labels": [{ "label_id": "e44099ec-1d4f-47bd-bd2c-800dd4c2f875", "name": "Opportunities" }] }, { "id": "52f2027f-9ce1-4c22-b19d-84ef8c6758a8", "name": "Initial Outreach and Engagement", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "a7604c1f-9d75-46d7-82cf-42f2c27c79d7", "status_name": "To Do", "phase_id": "4e0d6f73-8feb-40a1-bfe1-2fcb85a6c82e", "phase_name": "Lost", "labels": [{ "label_id": "4b163dcd-ac82-4a8c-a273-6816382ceb5b", "name": "Customer Retention" }] }, { "id": "f34842cb-03a2-4636-a288-4b93e65858d6", "name": "Lost Opportunity Analysis", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "a7604c1f-9d75-46d7-82cf-42f2c27c79d7", "status_name": "To Do", "phase_id": "4e0d6f73-8feb-40a1-bfe1-2fcb85a6c82e", "phase_name": "Lost", "labels": [{ "label_id": "810b5321-2a8b-4df4-9ec3-bea403953c14", "name": "Opportunity Management" }] }, { "id": "827194e1-dc66-4b6f-a6e4-0f2f2e6da509", "name": "Competitor Analysis and Benchmarking", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "ac628ff7-6fdc-4371-b0f6-5a093b9c50cf", "status_name": "Doing", "phase_id": "4e0d6f73-8feb-40a1-bfe1-2fcb85a6c82e", "phase_name": "Lost", "labels": [{ "label_id": "55bade0d-544f-4438-86a9-374623ad268e", "name": "Prospective Client" }] }, { "id": "2f4eb7ce-8d55-4f3d-9e95-5e833da8e09c", "name": "Post-Sale Follow-Up and Relationship Building", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "2dc0474f-5cac-4112-a904-fcbc109621e4", "status_name": "Done", "phase_id": "8e96a00f-f246-4c10-ac06-c2524568ab7e", "phase_name": "Won", "labels": [{ "label_id": "cf733edf-e7bc-48b1-b445-1e20903be1b3", "name": "Sales Metrics" }] }, { "id": "6765e6df-eae6-4338-bf0b-47cd6ab139ee", "name": "Testing and Quality Assurance", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "2dc0474f-5cac-4112-a904-fcbc109621e4", "status_name": "Done", "phase_id": "8e96a00f-f246-4c10-ac06-c2524568ab7e", "phase_name": "Won", "labels": [{ "label_id": "9ef2069e-553e-4680-92ed-4c33e4bc63b9", "name": "Marketing Analytics" }] }],
+ "phases": [{ "id": "2f135803-6ee4-4dd5-a5e5-65d630fa2f6a", "color_code": "#a9a9a9", "name": "Lead" }, { "id": "4e0d6f73-8feb-40a1-bfe1-2fcb85a6c82e", "color_code": "#a9a9a9", "name": "Lost" }, { "id": "8e96a00f-f246-4c10-ac06-c2524568ab7e", "color_code": "#a9a9a9", "name": "Won" }, { "id": "8b6e69af-82eb-4aeb-897f-918df3d108c3", "color_code": "#a9a9a9", "name": "Opportunity" }],
+ "status": [{ "id": "a7604c1f-9d75-46d7-82cf-42f2c27c79d7", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "ac628ff7-6fdc-4371-b0f6-5a093b9c50cf", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "2dc0474f-5cac-4112-a904-fcbc109621e4", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "0b6d84f1-7610-4c04-9c51-195fb0bfb54d",
+ "name": "Services & Consulting",
+ "key": "SCE",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "4508df40-a4c0-4e94-99d2-5b175cb702dd", "name": "Client Onboarding", "description": null, "total_minutes": 0, "sort_order": 0, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 0, "parent_task_id": null, "status_id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "status_name": "To Do", "phase_id": "6a671d12-69ce-4af1-813b-cef9bd460ef0", "phase_name": "Closing", "labels": [{ "label_id": "1f716400-0189-4ef0-9e75-3140d2425f03", "name": "Technology Integration" }] }, { "id": "501311fa-e795-4335-a039-44ca95db8acd", "name": "Project Documentation", "description": null, "total_minutes": 0, "sort_order": 1, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 1, "parent_task_id": null, "status_id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "status_name": "To Do", "phase_id": "23468cd0-a934-45f1-ae02-a6012501decd", "phase_name": "Initiation", "labels": [{ "label_id": "52a9ba92-4b3f-4507-b688-227f90085559", "name": "Retainer" }] }, { "id": "616f33b6-ee66-4b03-98c5-cbbafdc23292", "name": "Task Management", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "a0a2a4cb-f75c-4c02-81d0-341cb79d152a", "status_name": "Done", "phase_id": "23468cd0-a934-45f1-ae02-a6012501decd", "phase_name": "Initiation", "labels": [{ "label_id": "47f73127-df5f-4e89-8266-8a42ad7a7efc", "name": "Compliance Check" }] }, { "id": "2aca6114-d027-4525-a573-8886e784a20a", "name": "Resource Allocation", "description": null, "total_minutes": 0, "sort_order": 8, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 8, "parent_task_id": null, "status_id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "status_name": "To Do", "phase_id": "6a671d12-69ce-4af1-813b-cef9bd460ef0", "phase_name": "Closing", "labels": [{ "label_id": "efe84c59-e74d-4189-9282-b9a7f5bbf15b", "name": "Urgent" }] }, { "id": "e411971a-625e-46a5-bea0-82c8df39490e", "name": "Risk Assessment and Management", "description": null, "total_minutes": 0, "sort_order": 9, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 9, "parent_task_id": null, "status_id": "bfb22ebf-9c88-445a-b8ca-cd77acc913e6", "status_name": "Doing", "phase_id": "6a671d12-69ce-4af1-813b-cef9bd460ef0", "phase_name": "Closing", "labels": [{ "label_id": "275c4e52-a0d9-4b46-947b-765d2d672182", "name": "Pending Feedback" }] }, { "id": "d5aef302-590e-4190-bd21-5659dedf4b83", "name": "Time and Expense Tracking", "description": null, "total_minutes": 0, "sort_order": 2, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 2, "parent_task_id": null, "status_id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "status_name": "To Do", "phase_id": "51b810f2-8d40-4b90-97a9-3365588bd545", "phase_name": "Monitoring and Controlling", "labels": [{ "label_id": "1f335b92-8fe8-408f-9e49-b0c1ad77e721", "name": "Data Privacy" }] }, { "id": "e7a5d539-141c-4bb0-b558-254ce7b02f51", "name": "Continuous Improvement", "description": null, "total_minutes": 0, "sort_order": 3, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 3, "parent_task_id": null, "status_id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "status_name": "To Do", "phase_id": "e4fbeacb-0bbe-4e8b-92fc-a8fa4ca8b71e", "phase_name": "Planning", "labels": [{ "label_id": "cf8dccd1-4a12-4f61-800c-70bf5a11f0b1", "name": "Knowledge Sharing" }] }, { "id": "2343c762-8762-4d5f-98c7-289351b3e87b", "name": "Client Satisfaction and Feedback", "description": null, "total_minutes": 0, "sort_order": 5, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 5, "parent_task_id": null, "status_id": "bfb22ebf-9c88-445a-b8ca-cd77acc913e6", "status_name": "Doing", "phase_id": "e4fbeacb-0bbe-4e8b-92fc-a8fa4ca8b71e", "phase_name": "Planning", "labels": [{ "label_id": "47f73127-df5f-4e89-8266-8a42ad7a7efc", "name": "Compliance Check" }] }, { "id": "706d0be7-e42b-4847-8145-cd89ecfd7178", "name": "Client Communication", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "bfb22ebf-9c88-445a-b8ca-cd77acc913e6", "status_name": "Doing", "phase_id": "51b810f2-8d40-4b90-97a9-3365588bd545", "phase_name": "Monitoring and Controlling", "labels": [{ "label_id": "a0dfd5ed-ed0a-4998-a8ec-86680e57d418", "name": "Marketing Campaign" }] }, { "id": "8ef5f3e7-708b-46a4-9cb4-655f56932f6b", "name": "Project Planning", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "a0a2a4cb-f75c-4c02-81d0-341cb79d152a", "status_name": "Done", "phase_id": "1b9921ea-44ca-4d26-9adc-d3100df10db5", "phase_name": "Execution", "labels": [{ "label_id": "8c90bc05-03fa-4e01-9663-e40089fb4931", "name": "Payment Received" }] }],
+ "phases": [{ "id": "23468cd0-a934-45f1-ae02-a6012501decd", "color_code": "#a9a9a9", "name": "Initiation" }, { "id": "1b9921ea-44ca-4d26-9adc-d3100df10db5", "color_code": "#a9a9a9", "name": "Execution" }, { "id": "6a671d12-69ce-4af1-813b-cef9bd460ef0", "color_code": "#a9a9a9", "name": "Closing" }, { "id": "e4fbeacb-0bbe-4e8b-92fc-a8fa4ca8b71e", "color_code": "#a9a9a9", "name": "Planning" }, { "id": "51b810f2-8d40-4b90-97a9-3365588bd545", "color_code": "#a9a9a9", "name": "Monitoring and Controlling" }],
+ "status": [{ "id": "5c15a3dc-e1d9-4dff-a559-0bc2736254e9", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "bfb22ebf-9c88-445a-b8ca-cd77acc913e6", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "a0a2a4cb-f75c-4c02-81d0-341cb79d152a", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ },
+ {
+ "id": "9fd64e55-fc62-4e5f-98cb-97aadce669af",
+ "name": "Software Development",
+ "key": "SD",
+ "notes": null,
+ "phase_label": "Phase",
+ "tasks": [{ "id": "d14c22d0-c9d4-426e-821b-16065ab5df66", "name": "User Acceptance Testing", "description": null, "total_minutes": 0, "sort_order": 12, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 12, "parent_task_id": null, "status_id": "330044e8-2895-4b70-a1fa-fa95ca42d6e7", "status_name": "To Do", "phase_id": "e50f184a-a08e-48bc-814e-9d300520929f", "phase_name": "Backlog", "labels": [{ "label_id": "2901cd3c-6981-4c4c-9a7e-715990151a53", "name": "Testing" }] }, { "id": "08fe57ea-6f22-4c9e-b6f2-8abc39d89c33", "name": "Requirement Gathering", "description": null, "total_minutes": 0, "sort_order": 11, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 11, "parent_task_id": null, "status_id": "330044e8-2895-4b70-a1fa-fa95ca42d6e7", "status_name": "To Do", "phase_id": "e50f184a-a08e-48bc-814e-9d300520929f", "phase_name": "Backlog", "labels": [{ "label_id": "aceda8df-464b-4fdc-b57b-b16b0146a637", "name": "Improvement" }] }, { "id": "eb3a9d6c-020e-4335-ab6a-063e04ac0a75", "name": "Coding", "description": null, "total_minutes": 0, "sort_order": 4, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 4, "parent_task_id": null, "status_id": "330044e8-2895-4b70-a1fa-fa95ca42d6e7", "status_name": "To Do", "phase_id": "0e8cb5e8-2590-4675-ae10-7b0fbdf3fe18", "phase_name": "In Progress", "labels": [{ "label_id": "ee6ad60c-c3e1-422c-8149-06f2c794fe90", "name": "Feature" }] }, { "id": "3a724445-e36d-4a7d-9f9d-1c31fcc88773", "name": "Unit Testing", "description": null, "total_minutes": 0, "sort_order": 6, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 6, "parent_task_id": null, "status_id": "0f029ffc-1eb7-45f4-9d6c-08f01bf1b73f", "status_name": "Done", "phase_id": "8bc7699b-b757-41e0-be18-f87b5a86a2e3", "phase_name": "Closed", "labels": [{ "label_id": "2901cd3c-6981-4c4c-9a7e-715990151a53", "name": "Testing" }] }, { "id": "1b07ca9b-168a-407b-93dc-2a0a4d988086", "name": "Continuous Integration/Continuous Deployment (CI/CD)", "description": null, "total_minutes": 0, "sort_order": 7, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 7, "parent_task_id": null, "status_id": "972a47c5-9358-4203-a1bf-71b0722906b0", "status_name": "Doing", "phase_id": "0e8cb5e8-2590-4675-ae10-7b0fbdf3fe18", "phase_name": "In Progress", "labels": [{ "label_id": "0ead50d5-4daf-4541-a0ce-95db933bb13f", "name": "Developement" }] }, { "id": "de2874fd-9f00-4890-85a3-bdf8cfb54ae9", "name": "Code Review", "description": null, "total_minutes": 0, "sort_order": 10, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 10, "parent_task_id": null, "status_id": "972a47c5-9358-4203-a1bf-71b0722906b0", "status_name": "Doing", "phase_id": "b639b3cf-5e74-4d0d-8431-ea5f7fb476dd", "phase_name": "In Review", "labels": [{ "label_id": "a8f3b893-c782-41e2-8ea1-077b6a9865cb", "name": "Security" }] }, { "id": "04353528-ede7-4791-8a18-90fb79e71345", "name": "Collaboration and Communication", "description": null, "total_minutes": 0, "sort_order": 9, "priority_id": "e1b5e26c-2035-4e27-9d46-9304f332c131", "priority_name": "Medium", "new": 9, "parent_task_id": null, "status_id": "0f029ffc-1eb7-45f4-9d6c-08f01bf1b73f", "status_name": "Done", "phase_id": "b639b3cf-5e74-4d0d-8431-ea5f7fb476dd", "phase_name": "In Review", "labels": [{ "label_id": "aceda8df-464b-4fdc-b57b-b16b0146a637", "name": "Improvement" }] }],
+ "phases": [{ "id": "e50f184a-a08e-48bc-814e-9d300520929f", "color_code": "#a9a9a9", "name": "Backlog" }, { "id": "8bc7699b-b757-41e0-be18-f87b5a86a2e3", "color_code": "#a9a9a9", "name": "Closed" }, { "id": "0e8cb5e8-2590-4675-ae10-7b0fbdf3fe18", "color_code": "#a9a9a9", "name": "In Progress" }, { "id": "b639b3cf-5e74-4d0d-8431-ea5f7fb476dd", "color_code": "#a9a9a9", "name": "In Review" }],
+ "status": [{ "id": "330044e8-2895-4b70-a1fa-fa95ca42d6e7", "name": "To Do", "category_id": "66c914b9-efd1-438f-af71-fe6d12d10c10", "category_name": "To do" }, { "id": "972a47c5-9358-4203-a1bf-71b0722906b0", "name": "Doing", "category_id": "9d638e60-9cbe-4ad1-97db-c9de12529a09", "category_name": "Doing" }, { "id": "0f029ffc-1eb7-45f4-9d6c-08f01bf1b73f", "name": "Done", "category_id": "f80362b5-dd57-4a0d-977e-21b080e27c2c", "category_name": "Done" }]
+ }
+];
\ No newline at end of file
diff --git a/worklenz-backend/src/controllers/project-templates/pt-task-phases-controller.ts b/worklenz-backend/src/controllers/project-templates/pt-task-phases-controller.ts
new file mode 100644
index 00000000..e042b367
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/pt-task-phases-controller.ts
@@ -0,0 +1,104 @@
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import WorklenzControllerBase from "../worklenz-controller-base";
+
+export default class PtTaskPhasesController extends WorklenzControllerBase {
+
+ private static readonly DEFAULT_PHASE_COLOR = "#fbc84c";
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ if (!req.query.id)
+ return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
+
+ const q = `
+ INSERT INTO cpt_phases (name, color_code, template_id)
+ VALUES (CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM cpt_phases WHERE template_id = $2) + 1, ')'), $1,
+ $2)
+ RETURNING id, name, color_code;
+ `;
+
+ req.body.color_code = this.DEFAULT_PHASE_COLOR;
+
+ const result = await db.query(q, [req.body.color_code, req.query.id]);
+ const [data] = result.rows;
+
+ data.color_code = data.color_code + TASK_STATUS_COLOR_ALPHA;
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, name, color_code, (SELECT COUNT(*) FROM cpt_task_phases WHERE phase_id = cpt_phases.id) AS usage
+ FROM cpt_phases
+ WHERE template_id = $1
+ ORDER BY created_at DESC;
+ `;
+
+ const result = await db.query(q, [req.query.id]);
+
+ for (const phase of result.rows)
+ phase.color_code = phase.color_code + TASK_STATUS_COLOR_ALPHA;
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "PHASE_EXISTS_ERROR": `Phase name "{0}" already exists. Please choose a different name.`
+ }
+ })
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT update_phase_name($1, $2, $3);`;
+
+ const result = await db.query(q, [req.params.id, req.body.name.trim(), req.query.id]);
+ const [data] = result.rows;
+
+ data.update_phase_name.color_code = data.update_phase_name.color_code + TASK_STATUS_COLOR_ALPHA;
+
+ return res.status(200).send(new ServerResponse(true, data.update_phase_name));
+ }
+
+ @HandleExceptions()
+ public static async updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ UPDATE custom_project_templates
+ SET phase_label = $2
+ WHERE id = $1;
+ `;
+
+ const result = await db.query(q, [req.params.id, req.body.name.trim()]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async updateColor(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE cpt_phases SET color_code = $3 WHERE id = $1 AND template_id = $2 RETURNING id, name, color_code;`;
+ const result = await db.query(q, [req.params.id, req.query.id, req.body.color_code.substring(0, req.body.color_code.length - 2)]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ DELETE
+ FROM cpt_phases
+ WHERE id = $1
+ AND template_id = $2
+ `;
+
+ const result = await db.query(q, [req.params.id, req.query.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-templates/pt-task-statuses-controller.ts b/worklenz-backend/src/controllers/project-templates/pt-task-statuses-controller.ts
new file mode 100644
index 00000000..e6432b70
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/pt-task-statuses-controller.ts
@@ -0,0 +1,146 @@
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import WorklenzControllerBase from "../worklenz-controller-base";
+
+const existsErrorMessage = "At least one status should exists under each category.";
+
+export default class PtTaskStatusesController extends WorklenzControllerBase {
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ INSERT INTO cpt_task_statuses (name, template_id, team_id, category_id, sort_order)
+ VALUES ($1, $2, $3, $4, (SELECT MAX(sort_order) FROM cpt_task_statuses WHERE template_id = $2) + 1);
+ `;
+ const result = await db.query(q, [req.body.name, req.body.template_id, req.user?.team_id, req.body.category_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getCreated(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const team_id = req.user?.team_id;
+ const q = `SELECT create_pt_task_status($1, $2)`;
+ const result = await db.query(q, [JSON.stringify(req.body), team_id]);
+ const data = result.rows[0].create_pt_task_status[0];
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ if (!req.query.template_id)
+ return res.status(400).send(new ServerResponse(false, null));
+
+ const q = `
+ SELECT cpt_task_statuses.id,
+ cpt_task_statuses.name,
+ stsc.color_code,
+ stsc.name AS category_name,
+ cpt_task_statuses.category_id,
+ stsc.description
+ FROM cpt_task_statuses
+ INNER JOIN sys_task_status_categories stsc ON cpt_task_statuses.category_id = stsc.id
+ WHERE template_id = $1
+ AND team_id = $2
+ ORDER BY cpt_task_statuses.sort_order;
+ `;
+
+ const result = await db.query(q, [req.query.template_id, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+
+ @HandleExceptions()
+ public static async getCategories(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, color_code, description
+ FROM sys_task_status_categories
+ ORDER BY index;`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT cpt_task_statuses.id, cpt_task_statuses.name, stsc.color_code
+ FROM cpt_task_statuses
+ INNER JOIN sys_task_status_categories stsc ON cpt_task_statuses.category_id = stsc.id
+ WHERE cpt_task_statuses.id = $1
+ AND template_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.query.template_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ private static async hasMoreCategories(statusId: string, templateId: string) {
+ if (!statusId || !templateId)
+ return false;
+
+ const q = `
+ SELECT COUNT(*) AS count
+ FROM cpt_task_statuses
+ WHERE category_id = (SELECT category_id FROM cpt_task_statuses WHERE id = $1)
+ AND template_id = $2;
+ `;
+
+ const result = await db.query(q, [statusId, templateId]);
+ const [data] = result.rows;
+ return +data.count >= 2;
+ }
+
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const hasMoreCategories = await PtTaskStatusesController.hasMoreCategories(req.params.id, req.body.template_id);
+
+ if (!hasMoreCategories)
+ return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status update failed!"));
+
+ const q = `
+ UPDATE cpt_task_statuses
+ SET name = $2,
+ category_id = COALESCE($4, (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))
+ WHERE id = $1
+ AND template_id = $3
+ RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = cpt_task_statuses.category_id);
+ `;
+ const result = await db.query(q, [req.params.id, req.body.name, req.body.template_id, req.body.category_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "STATUS_EXISTS_ERROR": `Status name "{0}" already exists. Please choose a different name.`
+ }
+ })
+ public static async updateName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DO
+ $$
+ BEGIN
+ -- check whether the status name is already in
+ IF EXISTS(SELECT name
+ FROM cpt_task_statuses
+ WHERE name = '${req.body.name}'::TEXT
+ AND template_id = '${req.body.template_id}'::UUID)
+ THEN
+ RAISE 'STATUS_EXISTS_ERROR:%', ('${req.body.name}')::TEXT;
+ END IF;
+
+ UPDATE cpt_task_statuses
+ SET name = '${req.body.name}'::TEXT
+ WHERE id = '${req.params.id}'::UUID
+ AND template_id = '${req.body.template_id}'::UUID;
+ END
+ $$;`;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+}
\ No newline at end of file
diff --git a/worklenz-backend/src/controllers/project-templates/pt-tasks-controller-base.ts b/worklenz-backend/src/controllers/project-templates/pt-tasks-controller-base.ts
new file mode 100644
index 00000000..3868bb1b
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/pt-tasks-controller-base.ts
@@ -0,0 +1,56 @@
+import WorklenzControllerBase from "../worklenz-controller-base";
+import { getColor } from "../../shared/utils";
+import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
+
+export const GroupBy = {
+ STATUS: "status",
+ PRIORITY: "priority",
+ LABELS: "labels",
+ PHASE: "phase"
+};
+
+export interface ITaskGroup {
+ id?: string;
+ name: string;
+ start_date?: string;
+ end_date?: string;
+ color_code: string;
+ category_id: string | null;
+ old_category_id?: string;
+ todo_progress?: number;
+ doing_progress?: number;
+ done_progress?: number;
+ tasks: any[];
+}
+
+
+export default class PtTasksControllerBase extends WorklenzControllerBase {
+
+ public static updateTaskViewModel(task: any) {
+
+ task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
+
+ if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
+
+ task.is_sub_task = !!task.parent_task_id;
+
+ task.total_time_string = `${~~(task.total_minutes / 60)}h ${(task.total_minutes % 60)}m`;
+
+ task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
+ task.show_sub_tasks = false;
+
+ if (task.phase_id) {
+ task.phase_color = task.phase_color
+ ? task.phase_color + TASK_PRIORITY_COLOR_ALPHA : getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA;
+ }
+
+ task.all_labels = task.labels;
+ task.labels = PtTasksControllerBase.createTagList(task.labels, 2);
+
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
+
+ return task;
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts b/worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts
new file mode 100644
index 00000000..e87542d5
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/pt-tasks-controller.ts
@@ -0,0 +1,249 @@
+import { ParsedQs } from "qs";
+
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import PtTasksControllerBase, { GroupBy, ITaskGroup } from "./pt-tasks-controller-base";
+
+export class PtTaskListGroup implements ITaskGroup {
+ name: string;
+ category_id: string | null;
+ color_code: string;
+ tasks: any[];
+
+ constructor(group: any) {
+ this.name = group.name;
+ this.category_id = group.category_id || null;
+ this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
+ this.tasks = [];
+ }
+
+}
+
+export default class PtTasksController extends PtTasksControllerBase {
+ private static isCountsOnly(query: ParsedQs) {
+ return query.count === "true";
+ }
+
+ public static isTasksOnlyReq(query: ParsedQs) {
+ return PtTasksController.isCountsOnly(query) || query.parent_task;
+ }
+
+ private static flatString(text: string) {
+ return (text || "").split(" ").map(s => `'${s}'`).join(",");
+ }
+
+ private static getFilterByTemplatsWhereClosure(text: string) {
+ return text ? `template_id IN (${this.flatString(text)})` : "";
+ }
+
+ private static getQuery(userId: string, options: ParsedQs) {
+
+ const searchField = options.search ? "cptt.name" : "sort_order";
+ const { searchQuery, sortField } = PtTasksController.toPaginationOptions(options, searchField);
+
+ const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
+
+ const isSubTasks = !!options.parent_task;
+
+ const subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
+
+ return `
+ SELECT id,
+ name,
+ cptt.template_id AS template_id,
+ cptt.parent_task_id,
+ cptt.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT COUNT(*)
+ FROM cpt_tasks
+ WHERE parent_task_id = cptt.id)::INT AS sub_tasks_count,
+ cptt.status_id AS status,
+ cptt.description,
+ cptt.sort_order,
+ (SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id) AS phase_id,
+ (SELECT name
+ FROM cpt_phases
+ WHERE id = (SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id)) AS phase_name,
+ (SELECT color_code
+ FROM cpt_phases
+ WHERE id = (SELECT phase_id FROM cpt_task_phases WHERE task_id = cptt.id)) AS phase_color,
+
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = cptt.status_id)) AS status_color,
+
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM cpt_task_statuses WHERE id = cptt.status_id)) r) AS status_category,
+
+ (SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
+ FROM (SELECT cpt_task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = cpt_task_labels.label_id),
+ (SELECT color_code FROM team_labels WHERE id = cpt_task_labels.label_id)
+ FROM cpt_task_labels
+ WHERE task_id = cptt.id) r) AS labels,
+ (SELECT id FROM task_priorities WHERE id = cptt.priority_id) AS priority,
+ (SELECT value FROM task_priorities WHERE id = cptt.priority_id) AS priority_value,
+ total_minutes
+ FROM cpt_tasks cptt
+ WHERE cptt.template_id=$1 AND ${subTasksFilter} ${searchQuery}
+ ORDER BY ${sortFields}
+ `;
+ }
+
+ public static async getGroups(groupBy: string, templateId: string): Promise {
+ let q = "";
+ let params: any[] = [];
+ switch (groupBy) {
+ case GroupBy.STATUS:
+ q = `
+ SELECT id,
+ name,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = cpt_task_statuses.category_id),
+ category_id
+ FROM cpt_task_statuses
+ WHERE template_id = $1
+ ORDER BY sort_order;
+ `;
+ params = [templateId];
+ break;
+ case GroupBy.PRIORITY:
+ q = `SELECT id, name, color_code
+ FROM task_priorities
+ ORDER BY value DESC;`;
+ break;
+ case GroupBy.LABELS:
+ q = `
+ SELECT id, name, color_code
+ FROM team_labels
+ WHERE team_id = $2
+ AND EXISTS(SELECT 1
+ FROM cpt_tasks
+ WHERE template_id = $1
+ AND EXISTS(SELECT 1 FROM cpt_task_labels WHERE task_id = cpt_tasks.id AND label_id = team_labels.id))
+ ORDER BY name;
+ `;
+ break;
+ case GroupBy.PHASE:
+ q = `
+ SELECT id, name, color_code
+ FROM cpt_phases
+ WHERE template_id = $1
+ ORDER BY created_at DESC;
+ `;
+ params = [templateId];
+ break;
+
+ default:
+ break;
+ }
+
+ const result = await db.query(q, params);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const groupBy = (req.query.group || GroupBy.STATUS) as string;
+
+ const q = PtTasksController.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+
+ const result = await db.query(q, params);
+ const tasks = [...result.rows];
+
+ const groups = await this.getGroups(groupBy, req.params.id);
+ const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
+ if (group.id)
+ g[group.id] = new PtTaskListGroup(group);
+ return g;
+ }, {});
+
+ this.updateMapByGroup(tasks, groupBy, map);
+
+ const updatedGroups = Object.keys(map).map(key => {
+ const group = map[key];
+
+ return {
+ id: key,
+ ...group
+ };
+ });
+
+ return res.status(200).send(new ServerResponse(true, updatedGroups));
+ }
+
+ public static updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: ITaskGroup }) {
+ let index = 0;
+ const unmapped = [];
+ for (const task of tasks) {
+ task.index = index++;
+ PtTasksController.updateTaskViewModel(task);
+ if (groupBy === GroupBy.STATUS) {
+ map[task.status]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PRIORITY) {
+ map[task.priority]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PHASE && task.phase_id) {
+ map[task.phase_id]?.tasks.push(task);
+ } else {
+ unmapped.push(task);
+ }
+
+ const totalMinutes = task.total_minutes;
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+
+ task.total_hours = hours;
+ task.total_minutes = minutes;
+ }
+
+ if (unmapped.length) {
+ map[UNMAPPED] = {
+ name: UNMAPPED,
+ category_id: null,
+ color_code: "#fbc84c69",
+ tasks: unmapped
+ };
+ }
+ }
+
+ @HandleExceptions()
+ public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const q = PtTasksController.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+ const result = await db.query(q, params);
+
+ let data: any[] = [];
+
+ // if true, we only return the record count
+ if (this.isCountsOnly(req.query)) {
+ [data] = result.rows;
+ } else { // else we return a flat list of tasks
+ data = [...result.rows];
+ for (const task of data) {
+ PtTasksController.updateTaskViewModel(task);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async bulkDelete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const deletedTasks = req.body.tasks.map((t: any) => t.id);
+
+ const result: any = {deleted_tasks: deletedTasks};
+
+ const q = `SELECT bulk_delete_pt_tasks($1) AS task;`;
+ await db.query(q, [JSON.stringify(req.body)]);
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/project-templates/pt-templates-controller.ts b/worklenz-backend/src/controllers/project-templates/pt-templates-controller.ts
new file mode 100644
index 00000000..c9a74e6b
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-templates/pt-templates-controller.ts
@@ -0,0 +1,233 @@
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+
+import db from "../../config/db";
+import { ServerResponse } from "../../models/server-response";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { templateData } from "./project-templates";
+import ProjectTemplatesControllerBase from "./project-templates-base";
+import { LOG_DESCRIPTIONS, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
+import { IO } from "../../shared/io";
+
+export default class ProjectTemplatesController extends ProjectTemplatesControllerBase {
+
+ @HandleExceptions()
+ public static async getTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name FROM pt_project_templates ORDER BY name;`;
+ const result = await db.query(q, []);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getCustomTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery } = this.toPaginationOptions(req.query, "name");
+
+ const q = `SELECT id, name, created_at, FALSE AS selected FROM custom_project_templates WHERE team_id = $1 ${searchQuery} ORDER BY name;`;
+ const result = await db.query(q, [req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { id } = req.params;
+
+ const q = `DELETE FROM custom_project_templates WHERE id = $1;`;
+ await db.query(q, [id]);
+ return res.status(200).send(new ServerResponse(true, [], "Template deleted successfully."));
+ }
+
+ @HandleExceptions()
+ public static async getDefaultProjectStatus() {
+ const q = `SELECT id FROM sys_project_statuses WHERE is_default IS TRUE;`;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return data.id;
+ }
+
+ @HandleExceptions()
+ public static async getDefaultProjectHealth() {
+ const q = `SELECT id FROM sys_project_healths WHERE is_default IS TRUE`;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return data.id;
+ }
+
+ @HandleExceptions()
+ public static async getTemplateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { id } = req.params;
+ const data = await this.getTemplateData(id);
+
+ for (const phase of data.phases) {
+ phase.color_code = phase.color_code + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ for (const status of data.status) {
+ status.color_code = status.color_code + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ for (const priority of data.priorities) {
+ priority.color_code = priority.color_code + TASK_PRIORITY_COLOR_ALPHA;
+ }
+
+ for (const label of data.labels) {
+ label.color_code = label.color_code + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async createTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ for (const template of templateData) {
+ let template_id: string | null = null;
+ template_id = await this.insertProjectTemplate(template);
+ if (template_id) {
+ await this.insertTemplateProjectPhases(template.phases, template_id);
+ await this.insertTemplateProjectStatuses(template.status, template_id);
+ await this.insertTemplateProjectTasks(template.tasks, template_id);
+ }
+ }
+ return res.status(200).send(new ServerResponse(true, []));
+ }
+
+ @HandleExceptions()
+ public static async importTemplates(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { template_id } = req.body;
+ let project_id: string | null = null;
+
+ const data = await this.getTemplateData(template_id);
+ if (data) {
+ data.team_id = req.user?.team_id || null;
+ data.user_id = req.user?.id || null;
+ data.folder_id = null;
+ data.category_id = null;
+ data.status_id = await this.getDefaultProjectStatus();
+ data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
+ data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
+ data.health_id = await this.getDefaultProjectHealth();
+ data.working_days = 0;
+ data.man_days = 0;
+ data.hours_per_day = 8;
+
+ project_id = await this.importTemplate(data);
+
+ await this.insertTeamLabels(data.labels, req.user?.team_id);
+ await this.insertProjectPhases(data.phases, project_id as string);
+ await this.insertProjectTasks(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
+
+ return res.status(200).send(new ServerResponse(true, { project_id }));
+ }
+ return res.status(200).send(new ServerResponse(true, { project_id }));
+ }
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.`
+ }
+ })
+ public static async createCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { project_id, templateName, projectIncludes, taskIncludes } = req.body;
+ const team_id = req.user?.team_id || null;
+
+ if (!team_id || !project_id) return res.status(400).send(new ServerResponse(false, {}));
+
+
+ let status, labels, phases = [];
+
+ const data = await this.getProjectData(project_id);
+
+ if (projectIncludes.statuses) {
+ status = await this.getProjectStatus(project_id);
+ }
+ if (projectIncludes.phases) {
+ phases = await this.getProjectPhases(project_id);
+ }
+ if (projectIncludes.labels) {
+ labels = await this.getProjectLabels(team_id, project_id);
+ }
+
+ const tasks = await this.getTasksByProject(project_id, taskIncludes);
+
+ data.name = templateName;
+ data.team_id = team_id;
+
+ const q = `SELECT create_project_template($1);`;
+ const result = await db.query(q, [JSON.stringify(data)]);
+ const [obj] = result.rows;
+
+ const template_id = obj.create_project_template.id;
+
+ if (template_id) {
+ if (phases) await this.insertCustomTemplatePhases(phases, template_id);
+ if (status) await this.insertCustomTemplateStatus(status, template_id, team_id);
+ if (tasks) await this.insertCustomTemplateTasks(tasks, template_id, team_id);
+ }
+
+ return res.status(200).send(new ServerResponse(true, {}, "Project template created successfully."));
+ }
+
+ @HandleExceptions()
+ public static async setupAccount(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { template_id, team_name } = req.body;
+ let project_id: string | null = null;
+
+ await this.updateTeamName(team_name, req.user?.team_id as string, req.user?.id as string);
+
+ const data = await this.getTemplateData(template_id);
+ if (data) {
+ data.team_id = req.user?.team_id || null;
+ data.user_id = req.user?.id || null;
+ data.folder_id = null;
+ data.category_id = null;
+ data.status_id = await this.getDefaultProjectStatus();
+ data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
+ data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
+ data.health_id = await this.getDefaultProjectHealth();
+ data.working_days = 0;
+ data.man_days = 0;
+ data.hours_per_day = 8;
+
+ project_id = await this.importTemplate(data);
+
+ await this.insertTeamLabels(data.labels, req.user?.team_id);
+ await this.insertProjectPhases(data.phases, project_id as string);
+ await this.insertProjectTasks(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
+
+ await this.handleAccountSetup(project_id as string, data.user_id, team_name);
+
+ return res.status(200).send(new ServerResponse(true, { id: project_id }));
+ }
+ return res.status(200).send(new ServerResponse(true, { id: project_id }));
+ }
+
+ @HandleExceptions()
+ public static async importCustomTemplate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { template_id } = req.body;
+ let project_id: string | null = null;
+
+ const data = await this.getCustomTemplateData(template_id);
+ if (data) {
+ data.team_id = req.user?.team_id || null;
+ data.user_id = req.user?.id || null;
+ data.folder_id = null;
+ data.category_id = null;
+ data.status_id = await this.getDefaultProjectStatus();
+ data.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
+ data.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
+ data.working_days = 0;
+ data.man_days = 0;
+ data.hours_per_day = 8;
+
+ project_id = await this.importTemplate(data);
+
+ await this.deleteDefaultStatusForProject(project_id as string);
+ await this.insertTeamLabels(data.labels, req.user?.team_id);
+ await this.insertProjectPhases(data.phases, project_id as string);
+ await this.insertProjectStatuses(data.status, project_id as string, data.team_id );
+ await this.insertProjectTasksFromCustom(data.tasks, data.team_id, project_id as string, data.user_id, IO.getSocketById(req.user?.socket_id as string));
+
+ return res.status(200).send(new ServerResponse(true, { project_id }));
+ }
+ return res.status(200).send(new ServerResponse(true, { project_id }));
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-workload/workload-gannt-base.ts b/worklenz-backend/src/controllers/project-workload/workload-gannt-base.ts
new file mode 100644
index 00000000..cc81643a
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-workload/workload-gannt-base.ts
@@ -0,0 +1,64 @@
+import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import WorklenzControllerBase from ".././worklenz-controller-base";
+
+export const GroupBy = {
+ STATUS: "status",
+ PRIORITY: "priority",
+ LABELS: "labels",
+ PHASE: "phase"
+};
+
+export interface IWLTaskGroup {
+ id?: string;
+ name: string;
+ color_code: string;
+ category_id: string | null;
+ old_category_id?: string;
+ tasks: any[];
+ isExpand: boolean;
+}
+
+export default class WLTasksControllerBase extends WorklenzControllerBase {
+ protected static calculateTaskCompleteRatio(totalCompleted: number, totalTasks: number) {
+ if (totalCompleted === 0 && totalTasks === 0) return 0;
+ const ratio = ((totalCompleted / totalTasks) * 100);
+ return ratio == Infinity ? 100 : ratio.toFixed();
+ }
+
+ public static updateTaskViewModel(task: any) {
+ task.progress = ~~(task.total_minutes_spent / task.total_minutes * 100);
+ task.overdue = task.total_minutes < task.total_minutes_spent;
+
+ if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
+
+ task.is_sub_task = !!task.parent_task_id;
+
+ task.name_color = getColor(task.name);
+ task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
+ task.show_sub_tasks = false;
+
+ if (task.phase_id) {
+ task.phase_color = task.phase_name
+ ? getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA
+ : null;
+ }
+
+ if (Array.isArray(task.assignees)) {
+ for (const assignee of task.assignees) {
+ assignee.color_code = getColor(assignee.name);
+ }
+ }
+
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
+
+ const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
+ const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
+ task.complete_ratio = WLTasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
+ task.completed_count = totalCompleted;
+ task.total_tasks_count = totalTasks;
+
+ return task;
+ }
+}
diff --git a/worklenz-backend/src/controllers/project-workload/workload-gannt-controller.ts b/worklenz-backend/src/controllers/project-workload/workload-gannt-controller.ts
new file mode 100644
index 00000000..982ba0a9
--- /dev/null
+++ b/worklenz-backend/src/controllers/project-workload/workload-gannt-controller.ts
@@ -0,0 +1,844 @@
+import moment, { Moment } from "moment";
+import momentTime from "moment-timezone";
+import { ParsedQs } from "qs";
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import WLTasksControllerBase, { GroupBy, IWLTaskGroup } from "./workload-gannt-base";
+
+interface IWorkloadTask {
+ id: string;
+ name: string;
+ start_date: string;
+ end_date: string;
+ width: number;
+ left: number;
+}
+
+export class IWLTaskListGroup implements IWLTaskGroup {
+ name: string;
+ category_id: string | null;
+ color_code: string;
+ tasks: any[];
+ isExpand: boolean;
+
+ constructor(group: any) {
+ this.name = group.name;
+ this.category_id = group.category_id || null;
+ this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
+ this.tasks = [];
+ this.isExpand = group.isExpand;
+ }
+}
+
+export default class WorkloadGanntController extends WLTasksControllerBase {
+
+ private static GLOBAL_DATE_WIDTH = 30;
+ private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
+ private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
+
+ private static TASKS_START_DATE_NULL_FILTER = "start_date_null";
+ private static TASKS_END_DATE_NULL_FILTER = "end_date_null";
+ private static TASKS_START_END_DATES_NULL_FILTER = "start_end_dates_null";
+
+ private static async getFirstLastDates(projectId: string) {
+
+ const q = `SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(start_date) AS min_date, MAX(start_date) AS max_date
+ FROM tasks
+ WHERE project_id = $1 AND tasks.archived IS FALSE
+ UNION
+ SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
+ FROM tasks
+ WHERE project_id = $1 AND tasks.archived IS FALSE) AS date_union;`;
+
+ const res = await db.query(q, [projectId]);
+ return res.rows[0];
+ }
+
+ private static async getLogsFirstLastDates(projectId: string) {
+ const q = `SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS min_date,
+ MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS max_date
+ FROM task_work_log twl
+ INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
+ WHERE t.project_id = $1`;
+ const res = await db.query(q, [projectId]);
+ return res.rows[0];
+ }
+
+ private static validateEndDate(endDate: Moment): boolean {
+ return endDate.isBefore(moment(), "day");
+ }
+
+ private static validateStartDate(startDate: Moment): boolean {
+ return startDate.isBefore(moment(), "day");
+ }
+
+ private static getScrollAmount(startDate: Moment) {
+ const today = moment();
+ const daysDifference = today.diff(startDate, "days");
+
+ return (this.GLOBAL_DATE_WIDTH * daysDifference);
+ }
+
+ private static setTaskCss(task: IWorkloadTask) {
+ let startDate = task.start_date ? moment(task.start_date) : moment();
+ let endDate = task.end_date ? moment(task.end_date) : moment();
+
+ if (!task.start_date) {
+ startDate = moment(task.end_date);
+ }
+ if (!task.end_date) {
+ endDate = moment(task.start_date);
+ }
+ if (!task.start_date && !task.end_date) {
+ startDate = moment();
+ endDate = moment();
+ }
+
+ const daysDifferenceFromStart = startDate.diff(this.GLOBAL_START_DATE, "days");
+ task.left = daysDifferenceFromStart * this.GLOBAL_DATE_WIDTH;
+
+ if (moment(startDate).isSame(moment(endDate), "day")) {
+ task.width = this.GLOBAL_DATE_WIDTH;
+ } else {
+ const taskWidth = endDate.diff(startDate, "days");
+ task.width = (taskWidth + 1) * this.GLOBAL_DATE_WIDTH;
+ }
+
+ return task;
+ }
+
+ private static setIndicator(startDate: string, endDate: string) {
+ const daysFromStart = moment(startDate).diff(this.GLOBAL_START_DATE, "days");
+ const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
+
+ const daysDifference = moment(endDate).diff(startDate, "days");
+ const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
+
+ const body = {
+ indicatorOffset,
+ indicatorWidth
+ };
+
+ return body;
+
+ }
+
+ @HandleExceptions()
+ public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const dateRange = await this.getFirstLastDates(req.params.id as string);
+ const logRange = await this.getLogsFirstLastDates(req.params.id as string);
+
+ const today = new Date();
+
+ let startDate = moment(today).clone().startOf("month");
+ let endDate = moment(today).clone().endOf("month");
+
+ this.setChartStartEnd(dateRange, logRange, req.query.timeZone as string);
+
+ if (dateRange.start_date && dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
+ } else if (dateRange.start_date && !dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("month") : moment(today).clone().startOf("month");
+ } else if (!dateRange.start_date && dateRange.end_date) {
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("month") : moment(dateRange.end_date).endOf("month");
+ }
+
+ const xMonthsBeforeStart = startDate.clone().subtract(1, "months");
+ const xMonthsAfterEnd = endDate.clone().add(1, "months");
+
+ this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
+ this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
+
+ const dateData = [];
+ let days = -1;
+
+ const currentDate = xMonthsBeforeStart.clone();
+
+ while (currentDate.isBefore(xMonthsAfterEnd)) {
+ const monthData = {
+ month: currentDate.format("MMM YYYY"),
+ weeks: [] as number[],
+ days: [] as { day: number, name: string, isWeekend: boolean, isToday: boolean }[],
+ };
+ const daysInMonth = currentDate.daysInMonth();
+ for (let day = 1; day <= daysInMonth; day++) {
+ const dayOfMonth = currentDate.date();
+ const dayName = currentDate.format("ddd");
+ const isWeekend = [0, 6].includes(currentDate.day());
+ const isToday = moment(moment(today).format("YYYY-MM-DD")).isSame(moment(currentDate).format("YYYY-MM-DD"));
+ monthData.days.push({ day: dayOfMonth, name: dayName, isWeekend, isToday });
+ currentDate.add(1, "day");
+ days++;
+ }
+ dateData.push(monthData);
+ }
+
+ const scrollBy = this.getScrollAmount(xMonthsBeforeStart);
+
+ const result = {
+ date_data: dateData,
+ width: days + 1,
+ scroll_by: scrollBy,
+ chart_start: moment(this.GLOBAL_START_DATE).format("YYYY-MM-DD"),
+ chart_end: moment(this.GLOBAL_END_DATE).format("YYYY-MM-DD")
+ };
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ private static async setChartStartEnd(dateRange: any, logsRange: any, timeZone: string) {
+
+ if (dateRange.start_date)
+ dateRange.start_date = momentTime.tz(dateRange.start_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (dateRange.end_date)
+ dateRange.end_date = momentTime.tz(dateRange.end_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (logsRange.min_date)
+ logsRange.min_date = momentTime.tz(logsRange.min_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (logsRange.max_date)
+ logsRange.max_date = momentTime.tz(logsRange.max_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (moment(logsRange.min_date ).isBefore(dateRange.start_date))
+ dateRange.start_date = logsRange.min_date;
+
+ if (moment(logsRange.max_date ).isAfter(dateRange.endDate))
+ dateRange.end_date = logsRange.max_date;
+
+ return dateRange;
+ }
+
+ @HandleExceptions()
+ public static async getMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const expandedMembers: string[] = req.body.expanded_members;
+
+ const q = `SELECT pm.id AS project_member_id,
+ tmiv.team_member_id,
+ tmiv.user_id,
+ name AS name,
+ avatar_url,
+ TRUE AS project_member,
+
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(LEAST(start_date, end_date)) AS min_date,
+ MAX(GREATEST(start_date, end_date)) AS max_date
+ FROM tasks
+ INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE archived IS FALSE
+ AND project_id = $1
+ AND ta.team_member_id = tmiv.team_member_id) rec) AS duration,
+
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS min_date,
+ MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS max_date
+ FROM task_work_log twl
+ INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
+ WHERE t.project_id = $1
+ AND twl.user_id = tmiv.user_id) rec) AS logs_date_union,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT start_date,
+ end_date
+ FROM tasks
+ INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE archived IS FALSE
+ AND project_id = pm.project_id
+ AND ta.team_member_id = tmiv.team_member_id
+ ORDER BY start_date ASC) rec) AS tasks
+ FROM project_members pm
+ INNER JOIN team_member_info_view tmiv ON pm.team_member_id = tmiv.team_member_id
+ WHERE project_id = $1
+ ORDER BY (SELECT MIN(LEAST(start_date, end_date))
+ FROM tasks t
+ INNER JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE t.archived IS FALSE
+ AND t.project_id = $1
+ AND ta.team_member_id = tmiv.team_member_id) ASC NULLS LAST`;
+
+ const result = await db.query(q, [req.params.id]);
+
+ for (const member of result.rows) {
+ member.color_code = getColor(member.TaskName);
+
+ this.setMaxMinDate(member, req.query.timeZone as string);
+
+ // if (member.duration[0].min_date)
+ // member.duration[0].min_date = momentTime.tz(member.duration[0].min_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
+
+ // if (member.duration[0].max_date)
+ // member.duration[0].max_date = momentTime.tz(member.duration[0].max_date, `${req.query.timeZone}`).format("YYYY-MM-DD");
+
+ const fStartDate = member.duration.min_date ? moment(member.duration.min_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+ const fEndDate = member.duration.max_date ? moment(member.duration.max_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+
+ if (member.tasks.length > 0) {
+ const styles = this.setIndicator(fStartDate, fEndDate);
+ member.indicator_offset = styles.indicatorOffset;
+ member.indicator_width = styles.indicatorWidth;
+ member.not_allocated = false;
+ } else {
+ member.indicator_offset = 0;
+ member.indicator_width = 0;
+ member.not_allocated = true;
+ }
+
+ member.tasks_start_date = member.duration.min_date;
+ member.tasks_end_date = member.duration.max_date;
+ member.tasks_stats = await WorkloadGanntController.getMemberTasksStats(member.tasks);
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ private static async setMaxMinDate(member: any, timeZone: string) {
+
+ if (member.duration.min_date)
+ member.duration.min_date = momentTime.tz(member.duration.min_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (member.duration.max_date)
+ member.duration.max_date = momentTime.tz(member.duration.max_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (member.duration.min_date && member.duration.max_date && member.logs_date_union.min_date && member.logs_date_union.max_date) {
+
+ const durationMin = momentTime.tz(member.duration.min_date, `${timeZone}`).format("YYYY-MM-DD");
+ const durationMax = momentTime.tz(member.duration.max_date, `${timeZone}`).format("YYYY-MM-DD");
+ const logMin = momentTime.tz(member.logs_date_union.min_date, `${timeZone}`).format("YYYY-MM-DD");
+ const logMax = momentTime.tz(member.logs_date_union.max_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ if (moment(logMin).isBefore(durationMin)) {
+ member.duration.min_date = logMin;
+ }
+ if (moment(logMax).isAfter(durationMax)) {
+ member.duration.max_date = logMax;
+ }
+
+ return member;
+
+ }
+
+ if (!member.duration.min_date && !member.duration.max_date && member.logs_date_union.min_date && member.logs_date_union.max_date) {
+
+ const logMin = momentTime.tz(member.logs_date_union.min_date, `${timeZone}`).format("YYYY-MM-DD");
+ const logMax = momentTime.tz(member.logs_date_union.max_date, `${timeZone}`).format("YYYY-MM-DD");
+
+ member.duration.min_date = logMin;
+ member.duration.max_date = logMax;
+
+ return member;
+ }
+
+ return member;
+
+ }
+
+
+
+ private static async getMemberTasksStats(tasks: { start_date: string | null, end_date: string | null }[]) {
+ const tasksCount = tasks.length;
+ let nullStartCount = 0;
+ let nullEndCount = 0;
+ let nullBothCount = 0;
+
+ for (const task of tasks) {
+ if ((!task.start_date || task.start_date.trim() === "") && (!task.end_date || task.end_date.trim() === "")) {
+ nullBothCount++;
+ } else if ((!task.start_date || task.start_date.trim() === "") && (task.end_date)) {
+ nullStartCount++;
+ } else if ((!task.end_date || task.end_date.trim() === "") && (task.start_date)) {
+ nullEndCount++;
+ }
+ }
+
+ const body = {
+ total: tasksCount,
+ null_start_dates: nullStartCount,
+ null_end_dates: nullEndCount,
+ null_start_end_dates: nullBothCount,
+ null_start_dates_percentage: (nullStartCount / tasksCount) * 100,
+ null_end_dates_percentage: (nullEndCount / tasksCount) * 100,
+ null_start_end_dates_percentage: (nullBothCount / tasksCount) * 100,
+ available_start_end_dates_percentage: ((tasksCount - (nullStartCount + nullEndCount + nullBothCount)) / tasksCount) * 100
+ };
+ return body;
+ }
+
+ // ********************************************
+
+ private static isCountsOnly(query: ParsedQs) {
+ return query.count === "true";
+ }
+
+ public static isTasksOnlyReq(query: ParsedQs) {
+ return WorkloadGanntController.isCountsOnly(query) || query.parent_task;
+ }
+
+ private static flatString(text: string) {
+ return (text || "").split(" ").map(s => `'${s}'`).join(",");
+ }
+
+ private static getFilterByDatesWhereClosure(text: string) {
+ let closure = "";
+ switch (text.trim()) {
+ case "":
+ closure = ``;
+ break;
+ case WorkloadGanntController.TASKS_START_DATE_NULL_FILTER:
+ closure = `start_date IS NULL AND end_date IS NOT NULL`;
+ break;
+ case WorkloadGanntController.TASKS_END_DATE_NULL_FILTER:
+ closure = `start_date IS NOT NULL AND end_date IS NULL`;
+ break;
+ case WorkloadGanntController.TASKS_START_END_DATES_NULL_FILTER:
+ closure = `start_date IS NULL AND end_date IS NULL`;
+ break;
+ }
+ return closure;
+ }
+
+ private static getFilterByMembersWhereClosure(text: string) {
+ return text
+ ? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id IN (${this.flatString(text)}))`
+ : "";
+ }
+
+ private static getStatusesQuery(filterBy: string) {
+ return filterBy === "member"
+ ? `, (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code
+ FROM task_statuses
+ INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
+ WHERE project_id = t.project_id
+ ORDER BY task_statuses.name) rec) AS statuses`
+ : "";
+ }
+
+ public static async getTaskCompleteRatio(taskId: string): Promise<{
+ ratio: number;
+ total_completed: number;
+ total_tasks: number;
+ } | null> {
+ try {
+ const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
+ const [data] = result.rows;
+ data.info.ratio = +data.info.ratio.toFixed();
+ return data.info;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private static getQuery(userId: string, options: ParsedQs) {
+ const searchField = options.search ? "t.name" : "sort_order";
+ const { searchQuery, sortField } = WorkloadGanntController.toPaginationOptions(options, searchField);
+
+ const isSubTasks = !!options.parent_task;
+
+ const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
+ // Filter tasks by its members
+ const membersFilter = WorkloadGanntController.getFilterByMembersWhereClosure(options.members as string);
+ // Returns statuses of each task as a json array if filterBy === "member"
+ const statusesQuery = WorkloadGanntController.getStatusesQuery(options.filterBy as string);
+
+ const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
+
+ const datesFilter = WorkloadGanntController.getFilterByDatesWhereClosure(options.dateChecker as string);
+
+
+ let subTasksFilter;
+
+ if (options.isSubtasksInclude === "true") {
+ subTasksFilter = "";
+ } else {
+ subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
+ }
+
+ const filters = [
+ subTasksFilter,
+ (isSubTasks ? "1 = 1" : archivedFilter),
+ membersFilter,
+ datesFilter
+ ].filter(i => !!i).join(" AND ");
+
+ return `
+ SELECT id,
+ name,
+ t.project_id AS project_id,
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
+
+ t.status_id AS status,
+ t.archived,
+ t.sort_order,
+
+ (SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
+ (SELECT name
+ FROM project_phases
+ WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
+
+
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
+
+ (CASE
+ WHEN EXISTS(SELECT 1
+ FROM tasks_with_status_view
+ WHERE tasks_with_status_view.task_id = t.id
+ AND is_done IS TRUE) THEN 1
+ ELSE 0 END) AS parent_task_completed,
+ (SELECT get_task_assignees(t.id)) AS assignees,
+ (SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE tt.parent_task_id = t.id
+ AND tt.is_done IS TRUE)::INT
+ AS completed_sub_tasks,
+
+ (SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
+ (SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
+ total_minutes,
+ start_date,
+ end_date ${statusesQuery}
+ FROM tasks t
+ WHERE ${filters} ${searchQuery} AND project_id = $1
+ ORDER BY end_date DESC NULLS LAST
+ `;
+ }
+
+ public static async getGroups(groupBy: string, projectId: string): Promise {
+ let q = "";
+ let params: any[] = [];
+ switch (groupBy) {
+ case GroupBy.STATUS:
+ q = `
+ SELECT id,
+ name,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id),
+ category_id
+ FROM task_statuses
+ WHERE project_id = $1
+ ORDER BY sort_order;
+ `;
+ params = [projectId];
+ break;
+ case GroupBy.PRIORITY:
+ q = `SELECT id, name, color_code
+ FROM task_priorities
+ ORDER BY value DESC;`;
+ break;
+ case GroupBy.LABELS:
+ q = `
+ SELECT id, name, color_code
+ FROM team_labels
+ WHERE team_id = $2
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE project_id = $1
+ AND EXISTS(SELECT 1 FROM task_labels WHERE task_id = tasks.id AND label_id = team_labels.id))
+ ORDER BY name;
+ `;
+ break;
+ case GroupBy.PHASE:
+ q = `
+ SELECT id, name, color_code, start_date, end_date
+ FROM project_phases
+ WHERE project_id = $1
+ ORDER BY name;
+ `;
+ params = [projectId];
+ break;
+
+ default:
+ break;
+ }
+
+ const result = await db.query(q, params);
+ for (const row of result.rows) {
+ row.isExpand = true;
+ }
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const groupBy = (req.query.group || GroupBy.STATUS) as string;
+
+ const q = WorkloadGanntController.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+
+ const result = await db.query(q, params);
+ const tasks = [...result.rows];
+
+ const groups = await this.getGroups(groupBy, req.params.id);
+ const map = groups.reduce((g: { [x: string]: IWLTaskGroup }, group) => {
+ if (group.id)
+ g[group.id] = new IWLTaskListGroup(group);
+ return g;
+ }, {});
+
+ this.updateMapByGroup(tasks, groupBy, map);
+
+ const updatedGroups = Object.keys(map).map(key => {
+ const group = map[key];
+
+ if (groupBy === GroupBy.PHASE)
+ group.color_code = getColor(group.name) + TASK_PRIORITY_COLOR_ALPHA;
+
+ return {
+ id: key,
+ ...group
+ };
+ });
+
+ return res.status(200).send(new ServerResponse(true, updatedGroups));
+ }
+
+ public static updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: IWLTaskGroup }) {
+ let index = 0;
+ const unmapped = [];
+ for (const task of tasks) {
+ task.index = index++;
+ WorkloadGanntController.updateTaskViewModel(task);
+ if (groupBy === GroupBy.STATUS) {
+ map[task.status]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PRIORITY) {
+ map[task.priority]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PHASE && task.phase_id) {
+ map[task.phase_id]?.tasks.push(task);
+ } else {
+ unmapped.push(task);
+ }
+ }
+
+ if (unmapped.length) {
+ map[UNMAPPED] = {
+ name: UNMAPPED,
+ category_id: null,
+ color_code: "#f0f0f0",
+ tasks: unmapped,
+ isExpand: true
+ };
+ }
+ }
+
+
+ @HandleExceptions()
+ public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const q = WorkloadGanntController.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+ const result = await db.query(q, params);
+
+ let data: any[] = [];
+
+ // if true, we only return the record count
+ if (this.isCountsOnly(req.query)) {
+ [data] = result.rows;
+ } else { // else we return a flat list of tasks
+ data = [...result.rows];
+ for (const task of data) {
+ WorkloadGanntController.updateTaskViewModel(task);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const projectId = req.params.id;
+ const teamMemberId = req.query.team_member_id;
+
+ const getCountByStatus = await WorkloadGanntController.getTasksCountsByStatus(projectId, teamMemberId as string);
+ const getCountByPriority = await WorkloadGanntController.getTasksCountsByPriority(projectId, teamMemberId as string);
+ const getCountByPhase = await WorkloadGanntController.getTasksCountsByPhase(projectId, teamMemberId as string);
+ const getCountByDates = await WorkloadGanntController.getTasksCountsByDates(projectId, teamMemberId as string);
+ const data = {
+ by_status: getCountByStatus,
+ by_priority: getCountByPriority,
+ by_phase: getCountByPhase,
+ by_dates: getCountByDates
+ };
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ private static async getTasksCountsByStatus(projectId: string, teamMemberId: string) {
+ const q = `SELECT ts.id,
+ ts.name AS label,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = ts.category_id) AS color_code,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT COUNT(*)
+ FROM tasks t
+ WHERE t.project_id = $1
+ AND t.archived IS FALSE
+ AND t.id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND t.status_id = ts.id) rec) AS counts
+ FROM task_statuses ts
+ WHERE project_id = $1`;
+ const res = await db.query(q, [projectId, teamMemberId]);
+ for (const row of res.rows) {
+ row.tasks_count = row.counts[0].count;
+ }
+ return res.rows;
+ }
+
+ private static async getTasksCountsByPriority(projectId: string, teamMemberId: string) {
+ const q = `SELECT tp.id,
+ tp.name AS label,
+ tp.color_code,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT COUNT(*)
+ FROM tasks t
+ WHERE t.project_id = $1
+ AND t.archived IS FALSE
+ AND t.id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND t.priority_id = tp.id) rec) AS counts
+ FROM task_priorities tp`;
+ const res = await db.query(q, [projectId, teamMemberId]);
+ for (const row of res.rows) {
+ row.tasks_count = row.counts[0].count;
+ }
+ return res.rows;
+ }
+
+ private static async getTasksCountsByPhase(projectId: string, teamMemberId: string) {
+ const q = `SELECT pp.id,
+ pp.name AS label,
+ pp.color_code AS color_code,
+ COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON) AS counts
+ FROM project_phases pp
+ LEFT JOIN (SELECT pp.id AS phase_id,
+ COUNT(ta.task_id) AS task_count
+ FROM project_phases pp
+ LEFT JOIN task_phase tp ON pp.id = tp.phase_id
+ LEFT JOIN tasks t ON tp.task_id = t.id
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $2
+ WHERE pp.project_id = $1
+ GROUP BY pp.id ) rec ON pp.id = rec.phase_id
+ WHERE pp.project_id = $1
+ GROUP BY pp.id`;
+ const res = await db.query(q, [projectId, teamMemberId]);
+ for (const row of res.rows) {
+ row.tasks_count = row.counts[0].task_count;
+ row.color_code = getColor(row.label) + TASK_PRIORITY_COLOR_ALPHA;
+ }
+ return res.rows;
+ }
+
+ private static async getTasksCountsByDates(projectId: string, teamMemberId: string) {
+ const q = `SELECT JSON_BUILD_OBJECT(
+ 'having_start_end_date', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND archived IS FALSE
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND end_date IS NOT NULL AND start_date IS NOT NULL),
+ 'no_end_date', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND archived IS FALSE
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND end_date IS NULL AND start_date IS NOT NULL),
+ 'no_start_date', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND archived IS FALSE
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND end_date IS NOT NULL AND start_date IS NULL),
+ 'no_start_end_dates', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND archived IS FALSE
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = $2)
+ AND end_date IS NULL AND start_date IS NULL)) AS counts`;
+ const res = await db.query(q, [projectId, teamMemberId]);
+ const data = [
+ {
+ id: "",
+ label: "Having start & end date",
+ color_code: "#f0f0f0",
+ tasks_count: res.rows[0].counts.having_start_end_date
+ },
+ {
+ id: "",
+ label: "Without end date",
+ color_code: "#F9A0A0BF",
+ tasks_count: res.rows[0].counts.no_end_date
+ },
+ {
+ id: "",
+ label: "Without start date",
+ color_code: "#F8A9A98C",
+ tasks_count: res.rows[0].counts.no_start_date
+ },
+ {
+ id: "",
+ label: "Without start & end date",
+ color_code: "#F7A7A7E5",
+ tasks_count: res.rows[0].counts.no_start_end_dates
+ },
+ ];
+ return data;
+ }
+
+ // @HandleExceptions()
+ // public static async getTasksByTeamMeberId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ // const memberTasks = await this.getMemberTasks(req.params.id, req.body.team_member_id);
+ // return res.status(200).send(new ServerResponse(true, memberTasks));
+ // }
+
+ // private static async getMemberTasks(projectId: string, teamMemberId: string) {
+ // const q = `
+ // SELECT id AS task_id,
+ // name AS task_name,
+ // start_date AS start_date,
+ // end_date AS end_date
+ // FROM tasks
+ // INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ // WHERE archived IS FALSE
+ // AND project_id = $1
+ // AND ta.team_member_id = $2
+ // ORDER BY start_date ASC`;
+ // const result = await db.query(q, [projectId, teamMemberId]);
+
+ // for (const task of result.rows) {
+ // this.setTaskCss(task);
+ // }
+
+ // return result.rows;
+
+ // }
+
+}
diff --git a/worklenz-backend/src/controllers/projects-controller.ts b/worklenz-backend/src/controllers/projects-controller.ts
new file mode 100644
index 00000000..4e72c088
--- /dev/null
+++ b/worklenz-backend/src/controllers/projects-controller.ts
@@ -0,0 +1,744 @@
+import moment from "moment";
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {ServerResponse} from "../models/server-response";
+import {LOG_DESCRIPTIONS} from "../shared/constants";
+import {getColor} from "../shared/utils";
+import {generateProjectKey} from "../utils/generate-project-key";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import { NotificationsService } from "../services/notifications/notifications.service";
+import { IPassportSession } from "../interfaces/passport-session";
+import { SocketEvents } from "../socket.io/events";
+import { IO } from "../shared/io";
+
+export default class ProjectsController extends WorklenzControllerBase {
+
+ private static async getAllKeysByTeamId(teamId?: string) {
+ if (!teamId) return [];
+ try {
+ const result = await db.query("SELECT key FROM projects WHERE team_id = $1;", [teamId]);
+ return result.rows.map((project: any) => project.key).filter((key: any) => !!key);
+ } catch (error) {
+ return [];
+ }
+ }
+
+ private static async notifyProjecManagertUpdates(projectId: string, user: IPassportSession, projectManagerTeamMemberId: string | null) {
+
+ if (projectManagerTeamMemberId) {
+ const q = `SELECT (SELECT user_id FROM team_member_info_view WHERE team_member_id = $2) AS user_id,
+ (SELECT socket_id FROM users WHERE id = (SELECT user_id FROM team_member_info_view WHERE team_member_id = $2)) AS socket_id,
+ (SELECT name FROM projects WHERE id = $1) AS project_name
+ FROM project_members pm WHERE project_id = $1
+ AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')`;
+
+ const result = await db.query(q, [projectId, projectManagerTeamMemberId]);
+ const [data] = result.rows;
+
+ if (projectManagerTeamMemberId !== user.team_member_id) {
+ void NotificationsService.createNotification({
+ userId: data.user_id,
+ teamId: user?.team_id as string,
+ socketId: data.socket_id,
+ message: `You're assigned as the Project Manager of the ${data.project_name} .`,
+ taskId: null,
+ projectId: projectId as string
+ });
+ }
+ }
+
+ IO.getSocketById(user.socket_id as string)
+ ?.to(projectId)
+ .emit(SocketEvents.PROJECT_DATA_CHANGE.toString(), {user_id: user.id});
+
+ }
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "PROJECT_EXISTS_ERROR": `A project with the name "{0}" already exists. Please choose a different name.`
+ }
+ })
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT create_project($1) AS project`;
+
+ req.body.team_id = req.user?.team_id || null;
+ req.body.user_id = req.user?.id || null;
+
+ req.body.folder_id = req.body.folder_id || null;
+ req.body.category_id = req.body.category_id?.trim() || null;
+ req.body.client_name = req.body.client_name?.trim() || null;
+ req.body.project_created_log = LOG_DESCRIPTIONS.PROJECT_CREATED;
+ req.body.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
+ req.body.project_manager_id = req.body.project_manager ? req.body.project_manager.id : null;
+
+ const keys = await this.getAllKeysByTeamId(req.user?.team_id as string);
+ req.body.key = generateProjectKey(req.body.name, keys) || null;
+
+ const result = await db.query(q, [JSON.stringify(req.body)]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data.project || {}));
+ }
+
+ @HandleExceptions()
+ public static async updatePinnedView(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const projectId = req.body.project_id;
+ const teamMemberId = req.user?.team_member_id;
+ const defaultView = req.body.default_view;
+
+ const q = `UPDATE project_members SET default_view = $1 WHERE project_id = $2 AND team_member_id = $3`;
+ const result = await db.query(q, [defaultView, projectId, teamMemberId]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getMyProjectsToTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, color_code
+ FROM projects
+ WHERE team_id = $1
+ AND is_member_of_project(projects.id, $2, $1)`;
+ const result = await db.query(q, [req.user?.team_id, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getMyProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, size, offset} = this.toPaginationOptions(req.query, "name");
+
+ const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
+
+ const isArchived = req.query.filter === "2"
+ ? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
+ : ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS projects
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ EXISTS(SELECT user_id
+ FROM favorite_projects
+ WHERE user_id = '${req.user?.id}'
+ AND project_id = projects.id) AS favorite,
+ EXISTS(SELECT user_id
+ FROM archived_projects
+ WHERE user_id = '${req.user?.id}'
+ AND project_id = projects.id) AS archived,
+ color_code,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_id = projects.id) AS members_count,
+ (SELECT get_project_members(projects.id)) AS names,
+ (SELECT CASE
+ WHEN ((SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) >
+ updated_at)
+ THEN (SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id)
+ ELSE updated_at END) AS updated_at
+ FROM projects
+ WHERE team_id = $1 ${isArchived} ${isFavorites} ${searchQuery}
+ AND is_member_of_project(projects.id
+ , '${req.user?.id}'
+ , $1)
+ ORDER BY updated_at DESC
+ LIMIT $2 OFFSET $3) t) AS data
+ FROM projects
+ WHERE team_id = $1 ${isArchived} ${isFavorites} ${searchQuery}
+ AND is_member_of_project(projects.id
+ , '${req.user?.id}'
+ , $1)) rec;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, size, offset]);
+ const [data] = result.rows;
+ const projects = Array.isArray(data?.projects.data) ? data?.projects.data : [];
+ for (const project of projects) {
+ project.progress = project.all_tasks_count > 0
+ ? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
+
+ }
+ return res.status(200).send(new ServerResponse(true, data?.projects || this.paginatedDatasetDefaultStruct));
+ }
+
+ private static flatString(text: string) {
+ return (text || "").split(" ").map(s => `'${s}'`).join(",");
+ }
+
+ private static getFilterByCategoryWhereClosure(text: string) {
+ return text ? `AND category_id IN (${this.flatString(text)})` : "";
+ }
+
+ private static getFilterByStatusWhereClosure(text: string) {
+ return text ? `AND status_id IN (${this.flatString(text)})` : "";
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
+
+ const filterByMember = !req.user?.owner && !req.user?.is_admin ?
+ ` AND is_member_of_project(projects.id, '${req.user?.id}', $1) ` : "";
+
+ const isFavorites = req.query.filter === "1" ? ` AND EXISTS(SELECT user_id FROM favorite_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)` : "";
+ const isArchived = req.query.filter === "2"
+ ? ` AND EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`
+ : ` AND NOT EXISTS(SELECT user_id FROM archived_projects WHERE user_id = '${req.user?.id}' AND project_id = projects.id)`;
+ const categories = this.getFilterByCategoryWhereClosure(req.query.categories as string);
+ const statuses = this.getFilterByStatusWhereClosure(req.query.statuses as string);
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS projects
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT name FROM sys_project_statuses WHERE id = status_id) AS status,
+ (SELECT color_code FROM sys_project_statuses WHERE id = status_id) AS status_color,
+ (SELECT icon FROM sys_project_statuses WHERE id = status_id) AS status_icon,
+ EXISTS(SELECT user_id
+ FROM favorite_projects
+ WHERE user_id = '${req.user?.id}'
+ AND project_id = projects.id) AS favorite,
+ EXISTS(SELECT user_id
+ FROM archived_projects
+ WHERE user_id = '${req.user?.id}'
+ AND project_id = projects.id) AS archived,
+ color_code,
+ start_date,
+ end_date,
+ category_id,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_id = projects.id) AS members_count,
+ (SELECT get_project_members(projects.id)) AS names,
+ (SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
+ (SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
+ (SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
+ (SELECT color_code
+ FROM project_categories
+ WHERE id = projects.category_id) AS category_color,
+
+ ((SELECT team_member_id as team_member_id
+ FROM project_members
+ WHERE project_id = projects.id
+ AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))) AS project_manager_team_member_id,
+
+ (SELECT default_view
+ FROM project_members prm
+ WHERE prm.project_id = projects.id
+ AND team_member_id = '${req.user?.team_member_id}') AS team_member_default_view,
+
+ (SELECT CASE
+ WHEN ((SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) >
+ updated_at)
+ THEN (SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id)
+ ELSE updated_at END) AS updated_at
+ FROM projects
+ WHERE team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}
+ ORDER BY ${sortField} ${sortOrder}
+ LIMIT $2 OFFSET $3) t) AS data
+ FROM projects
+ WHERE team_id = $1 ${categories} ${statuses} ${isArchived} ${isFavorites} ${filterByMember} ${searchQuery}) rec;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, size, offset]);
+ const [data] = result.rows;
+
+ for (const project of data?.projects.data || []) {
+ project.progress = project.all_tasks_count > 0
+ ? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
+
+ project.updated_at_string = moment(project.updated_at).fromNow();
+
+ project.names = this.createTagList(project?.names);
+ project.names.map((a: any) => a.color_code = getColor(a.name));
+
+ if (project.project_manager_team_member_id) {
+ project.project_manager = {
+ id : project.project_manager_team_member_id
+ };
+ }
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, data?.projects || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getMembersByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {sortField, sortOrder, size, offset} = this.toPaginationOptions(req.query, "name");
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS members
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT project_members.id,
+ team_member_id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id) AS email,
+ u.avatar_url,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = project_members.project_id
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE tasks_assignees.project_member_id = project_members.id)) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = project_members.project_id
+ AND id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE tasks_assignees.project_member_id = project_members.id)
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE category_id = (SELECT id
+ FROM sys_task_status_categories
+ WHERE is_done IS TRUE))) AS completed_tasks_count,
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = project_members.team_member_id
+ AND email_invitations.team_id = $2) AS pending_invitation,
+ (SELECT project_access_levels.name
+ FROM project_access_levels
+ WHERE project_access_levels.id = project_members.project_access_level_id) AS access,
+ (SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
+ FROM project_members
+ INNER JOIN team_members tm ON project_members.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = $1
+ ORDER BY ${sortField} ${sortOrder}
+ LIMIT $3 OFFSET $4) t) AS data
+ FROM project_members
+ WHERE project_id = $1) rec;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, size, offset]);
+ const [data] = result.rows;
+
+ for (const member of data?.members.data || []) {
+ member.progress = member.all_tasks_count > 0
+ ? ((member.completed_tasks_count / member.all_tasks_count) * 100).toFixed(0) : 0;
+ }
+
+ return res.status(200).send(new ServerResponse(true, data?.members || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT projects.id,
+ projects.name,
+ projects.color_code,
+ projects.notes,
+ projects.key,
+ projects.start_date,
+ projects.end_date,
+ projects.status_id,
+ projects.health_id,
+ projects.created_at,
+ projects.updated_at,
+ projects.folder_id,
+ projects.phase_label,
+ projects.category_id,
+ (projects.estimated_man_days) AS man_days,
+ (projects.estimated_working_days) AS working_days,
+ (projects.hours_per_day) AS hours_per_day,
+ (SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
+ (SELECT color_code
+ FROM project_categories
+ WHERE id = projects.category_id) AS category_color,
+ (EXISTS(SELECT 1 FROM project_subscribers WHERE project_id = $1 AND user_id = $3)) AS subscribed,
+ (SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
+ sps.name AS status,
+ sps.color_code AS status_color,
+ sps.icon AS status_icon,
+ (SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
+
+ (SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
+ FROM (SELECT team_member_id AS id,
+ (SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
+ FROM (SELECT name,
+ email,
+ avatar_url
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_member_id = pm.team_member_id
+ AND tmiv.team_id = (SELECT team_id FROM projects WHERE id = $1)) pmi) AS project_manager_info,
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = pm.team_member_id
+ AND email_invitations.team_id = (SELECT team_id
+ FROM team_member_info_view
+ WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
+ (SELECT active FROM team_members WHERE id = pm.team_member_id)
+ FROM project_members pm
+ WHERE project_id = $1
+ AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager
+ FROM projects
+ LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
+ WHERE projects.id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id ?? null, req.user?.id ?? null]);
+ const [data] = result.rows;
+
+ if (data && data.project_manager) {
+ data.project_manager.name = data.project_manager.project_manager_info.name;
+ data.project_manager.email = data.project_manager.project_manager_info.email;
+ data.project_manager.avatar_url = data.project_manager.project_manager_info.avatar_url;
+ data.project_manager.color_code = getColor(data.project_manager.name);
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions({
+ raisedExceptions: {
+ "PROJECT_EXISTS_ERROR": `Project with "{0}" name already exists. Please choose a different project name.`
+ }
+ })
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT update_project($1) AS project;`;
+
+ const key = req.body.key?.toString().trim().toUpperCase();
+
+ if (!key)
+ return res.status(200).send(new ServerResponse(false, null, "The project key cannot be empty."));
+
+ if (key.length > 5)
+ return res.status(200).send(new ServerResponse(false, null, "The project key length cannot exceed 5 characters."));
+
+ req.body.id = req.params.id;
+ req.body.team_id = req.user?.team_id || null;
+ req.body.user_id = req.user?.id || null;
+ req.body.folder_id = req.body.folder_id || null;
+ req.body.category_id = req.body.category_id || null;
+ req.body.client_name = req.body.client_name?.trim() || null;
+ req.body.project_created_log = LOG_DESCRIPTIONS.PROJECT_UPDATED;
+ req.body.project_member_added_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_ADDED;
+ req.body.project_member_removed_log = LOG_DESCRIPTIONS.PROJECT_MEMBER_REMOVED;
+ req.body.team_member_id = req.body.project_manager ? req.body.project_manager.id : null;
+
+ const result = await db.query(q, [JSON.stringify(req.body)]);
+ const [data] = result.rows;
+
+ this.notifyProjecManagertUpdates(req.params.id, req.user as IPassportSession, req.body.project_manager ? req.body.project_manager.id : null);
+
+ return res.status(200).send(new ServerResponse(true, data.project));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE
+ FROM projects
+ WHERE id = $1
+ AND team_id = $2`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT (SELECT COUNT(id)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = $1
+ AND status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id =
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS done_task_count,
+
+ (SELECT COUNT(id)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = $1
+ AND status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE is_doing IS TRUE
+ OR is_todo IS TRUE))) AS pending_task_count
+ FROM projects
+ WHERE id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getOverviewMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {archived} = req.query;
+ const q = `
+ SELECT team_member_id AS id,
+ FALSE AS active,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END) AS project_task_count,
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ WHERE CASE
+ WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND project_member_id = project_members.id) AS task_count,
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN task_statuses ts ON t.status_id = ts.id
+ WHERE CASE
+ WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND project_member_id = project_members.id
+ AND ts.category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_task_count,
+
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN task_statuses ts ON t.status_id = ts.id
+ WHERE CASE
+ WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND project_member_id = project_members.id
+ AND end_date::DATE < CURRENT_DATE::DATE
+ AND t.status_id NOT IN (SELECT id
+ FROM task_statuses
+ WHERE category_id NOT IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))) AS overdue_task_count,
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ INNER JOIN tasks t ON tasks_assignees.task_id = t.id
+ INNER JOIN task_statuses ts ON t.status_id = ts.id
+ WHERE CASE
+ WHEN ($2 IS TRUE) THEN t.project_id IS NOT NULL
+ ELSE archived IS FALSE END
+ AND project_member_id = project_members.id
+ AND ts.category_id IN
+ (SELECT id
+ FROM sys_task_status_categories
+ WHERE is_doing IS TRUE
+ OR is_todo IS TRUE)) AS pending_task_count,
+ (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id),
+ u.avatar_url,
+ (SELECT team_member_info_view.email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ (SELECT name FROM job_titles WHERE id = tm.job_title_id) AS job_title
+ FROM project_members
+ INNER JOIN team_members tm ON project_members.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE project_id = $1;
+ `;
+ const result = await db.query(q, [req.params.id, archived === "true"]);
+
+ for (const item of result.rows) {
+ item.progress =
+ item.task_count > 0
+ ? ((item.done_task_count / item.task_count) * 100).toFixed(0)
+ : 0;
+ item.contribution =
+ item.project_task_count > 0
+ ? ((item.task_count / item.project_task_count) * 100).toFixed(0)
+ : 0;
+ item.tasks = [];
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getAllTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {searchQuery, size, offset} = this.toPaginationOptions(req.query, ["tasks.name"]);
+ const filterByMember = !req.user?.owner && !req.user?.is_admin ?
+ ` AND is_member_of_project(p.id, '${req.user?.id}', $1) ` : "";
+
+ const isDueSoon = req.query.filter == "1";
+
+ const dueSoon = isDueSoon ? "AND tasks.end_date IS NOT NULL" : "";
+ const orderBy = isDueSoon ? "tasks.end_date DESC" : "p.name";
+ const assignedToMe = req.query.filter == "2" ? `
+ AND tasks.id IN (SELECT task_id
+ FROM tasks_assignees
+ WHERE team_member_id = (SELECT id
+ FROM team_members
+ WHERE user_id = '${req.user?.id}'
+ AND team_id = $1))
+ ` : "";
+
+ const q = `
+ SELECT ROW_TO_JSON(rec) AS projects
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT tasks.id,
+ tasks.name,
+ p.team_id,
+ p.name AS project_name,
+ tasks.start_date,
+ tasks.end_date,
+ p.id AS project_id,
+ p.color_code AS project_color,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
+ (SELECT get_task_assignees(tasks.id)) AS names
+ FROM tasks
+ INNER JOIN projects p ON tasks.project_id = p.id
+ WHERE tasks.archived IS FALSE
+ AND p.team_id = $1 ${filterByMember} ${dueSoon} ${searchQuery} ${assignedToMe}
+ ORDER BY ${orderBy}
+ LIMIT $2 OFFSET $3) t) AS data
+ FROM tasks
+ INNER JOIN projects p ON tasks.project_id = p.id
+ WHERE tasks.archived IS FALSE
+ AND p.team_id = $1 ${filterByMember} ${dueSoon} ${searchQuery} ${assignedToMe}) rec;
+ `;
+ const result = await db.query(q, [req.user?.team_id || null, size, offset]);
+ const [data] = result.rows;
+
+ for (const project of data?.projects.data || []) {
+ project.progress = project.all_tasks_count > 0
+ ? ((project.completed_tasks_count / project.all_tasks_count) * 100).toFixed(0) : 0;
+
+ project.names = this.createTagList(project?.names);
+ project.names.map((a: any) => a.color_code = getColor(a.name));
+ }
+
+ return res.status(200).send(new ServerResponse(true, data.projects || this.paginatedDatasetDefaultStruct));
+ }
+
+ @HandleExceptions()
+ public static async getAllProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id AS value, name AS text
+ FROM projects
+ WHERE team_id = $1
+ ORDER BY name;`;
+ const result = await db.query(q, [req.user?.team_id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async toggleFavorite(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT toggle_favorite_project($1, $2);`;
+ const result = await db.query(q, [req.user?.id, req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async toggleArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT toggle_archive_project($1, $2);`;
+ const result = await db.query(q, [req.user?.id, req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async toggleArchiveAll(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT toggle_archive_all_projects($1);`;
+ const result = await db.query(q, [req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ public static async getProjectManager(projectId: string) {
+ const q = `SELECT team_member_id FROM project_members WHERE project_id = $1 AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')`;
+ const result = await db.query(q, [projectId]);
+ return result.rows || [];
+ }
+
+ public static async updateExistPhaseColors() {
+ const q = `SELECT id, name FROM project_phases`;
+ const phases = await db.query(q);
+
+ phases.rows.forEach((phase) => {
+ phase.color_code = getColor(phase.name);
+ });
+
+ const body = {
+ phases: phases.rows
+ };
+
+ const q2 = `SELECT update_existing_phase_colors($1)`;
+ await db.query(q2, [JSON.stringify(body)]);
+
+ }
+
+ public static async updateExistSortOrder() {
+ const q = `SELECT id, project_id FROM project_phases ORDER BY name`;
+ const phases = await db.query(q);
+
+ const sortNumbers: any = {};
+
+ phases.rows.forEach(phase => {
+ const projectId = phase.project_id;
+
+ if (!sortNumbers[projectId]) {
+ sortNumbers[projectId] = 0;
+ }
+
+ phase.sort_number = sortNumbers[projectId]++;
+ });
+
+ const body = {
+ phases: phases.rows
+ };
+
+ const q2 = `SELECT update_existing_phase_sort_order($1)`;
+ await db.query(q2, [JSON.stringify(body)]);
+ // return phases;
+
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting-controller.ts b/worklenz-backend/src/controllers/reporting-controller.ts
new file mode 100644
index 00000000..6825082a
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting-controller.ts
@@ -0,0 +1,1844 @@
+import moment from "moment";
+import Excel from "exceljs";
+
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import { formatDuration, getColor, log_error } from "../shared/utils";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
+
+const YESTERDAY = "YESTERDAY";
+const LAST_WEEK = "LAST_WEEK";
+const LAST_MONTH = "LAST_MONTH";
+const LAST_QUARTER = "LAST_QUARTER";
+
+export default class ReportingController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = ``;
+ const result = await db.query(q, []);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getEstimatedVsActualTime(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const projectStatus = (req.body.projectStatus || []) as string[]; // ids
+ const projectStatusIds = projectStatus.map(id => `'${id}'`).join(",");
+
+ if (!teams.length || !projectStatus.length) {
+ return res.status(200).send(new ServerResponse(true, {}));
+ }
+
+ const q = `SELECT id,
+ name,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id = projects.id))::INT AS total_logged_time,
+ (SELECT SUM(total_minutes * 60)
+ FROM tasks
+ WHERE project_id = projects.id)::INT AS total_estimated_time,
+ (SELECT COUNT(*) FROM tasks WHERE project_id = projects.id)
+ FROM projects
+ WHERE team_id IN (${teamIds})
+ AND status_id IN (${projectStatusIds})
+ ORDER BY name;`;
+ const result = await db.query(q, []);
+
+ const selectedProjects: any = [];
+ const estimated: any = [];
+ const estimated_string: any = [];
+ const logged: any = [];
+ const logged_string: any = [];
+
+ if (result.rows.length) {
+ result.rows.forEach((element: { name: string; total_estimated_time: string; total_logged_time: string }) => {
+ selectedProjects.push(element.name);
+ estimated.push(element.total_estimated_time ? parseFloat(moment.duration(element.total_estimated_time, "seconds").asHours().toFixed(2)) : 0);
+ logged.push(element.total_logged_time ? parseFloat(moment.duration(element.total_logged_time, "seconds").asHours().toFixed(2)) : 0);
+ estimated_string.push(formatDuration(moment.duration(element.total_logged_time || "0", "seconds")));
+ logged_string.push(formatDuration(moment.duration(element.total_logged_time || "0", "seconds")));
+ });
+ }
+
+ return res.status(200).send(new ServerResponse(true, {
+ projects: selectedProjects,
+ estimated,
+ logged,
+ estimated_string,
+ logged_string
+ }));
+ }
+
+ private static getDateRangeClause(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `task_work_log.created_at::DATE = '${start}'::DATE`;
+ }
+
+ return `task_work_log.created_at::DATE >= '${start}'::DATE AND task_work_log.created_at < '${end}'::DATE + INTERVAL '1 day'`;
+ }
+
+ if (key === YESTERDAY)
+ return "task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
+ if (key === LAST_WEEK)
+ return "task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
+ if (key === LAST_MONTH)
+ return "task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
+ if (key === LAST_QUARTER)
+ return "task_work_log.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
+
+ return null;
+ }
+
+ private static async getTimeLoggesByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false) {
+ try {
+ const projectIds = projects.map(p => `'${p}'`).join(",");
+ const userIds = users.map(u => `'${u}'`).join(",");
+
+ const duration = ReportingController.getDateRangeClause(key || LAST_WEEK, dateRange);
+
+ const q = `
+ SELECT projects.name,
+ projects.color_code,
+ sps.name AS status_name,
+ sps.color_code AS status_color_code,
+ sps.icon AS status_icon,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (
+ --
+ SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
+ FROM (
+ --
+ SELECT name,
+ (SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ LEFT JOIN tasks t ON task_work_log.task_id = t.id
+ WHERE user_id = users.id
+ AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
+ AND t.project_id = projects.id
+ AND ${duration}) AS time_logged
+ FROM users
+ WHERE id IN (${userIds})
+ ORDER BY name
+ --
+ ) r
+ --
+ ) AS time_logs
+ FROM projects
+ LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
+ WHERE projects.id IN (${projectIds})
+ AND EXISTS(SELECT 1
+ FROM task_work_log
+ LEFT JOIN tasks t ON task_work_log.task_id = t.id
+ WHERE CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
+ AND t.project_id = projects.id
+ AND ${duration});
+ `;
+
+ const result = await db.query(q, [archived]);
+
+ const format = (seconds: number) => {
+ const duration = moment.duration(seconds, "seconds");
+ const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
+ return formattedDuration;
+ };
+
+ for (const project of result.rows) {
+ if (project.all_tasks_count > 0) {
+ project.progress = Math.round((project.completed_tasks_count / project.all_tasks_count) * 100);
+ } else {
+ project.progress = 0;
+ }
+
+ let total = 0;
+ for (const log of project.time_logs) {
+ total += log.time_logged;
+ log.time_logged = format(log.time_logged);
+ }
+ project.total = format(total);
+ }
+
+ return result.rows;
+ } catch (error) {
+ log_error(error);
+ }
+ return [];
+ }
+
+ @HandleExceptions()
+ public static async getReportingOverviewStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT get_reporting_overview_stats($1, $2);`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data.get_reporting_overview_stats || {}));
+ }
+
+ @HandleExceptions()
+ public static async getReportingProjectStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT get_reporting_projects_stats($1, $2) AS stats;`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+ const [data] = result.rows;
+
+ const { total_estimated, total_logged } = data.stats;
+
+ data.stats.progress = data.stats.all_tasks_count > 0
+ ? ((data.stats.completed_tasks_count / data.stats.all_tasks_count) * 100).toFixed(0) : 0;
+
+ const totalMinutes = moment.duration(total_estimated, "seconds");
+ const totalSeconds = moment.duration(total_logged, "seconds");
+
+ data.stats.total_estimated_hours_string = formatDuration(totalMinutes);
+ data.stats.total_logged_hours_string = formatDuration(totalSeconds);
+
+ data.stats.overlogged_hours = formatDuration(totalMinutes.subtract(totalSeconds));
+
+ return res.status(200).send(new ServerResponse(true, data.stats || {}));
+ }
+
+ @HandleExceptions()
+ public static async getReportingTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT t.id,
+ t.name,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE projects.team_id = tm.team_id
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = $1) END) AS projects_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT CASE
+ WHEN u.name IS NOT NULL THEN u.name
+ ELSE (SELECT name
+ FROM email_invitations
+ WHERE team_member_id = team_members.id) END,
+ avatar_url
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ WHERE team_id = t.id) rec) AS team_members
+ FROM teams t
+ CROSS JOIN team_members tm
+ WHERE tm.team_id = t.id
+ AND tm.user_id = $1
+ AND role_id IN
+ (SELECT id FROM roles WHERE roles.team_id = t.id AND (admin_role IS TRUE OR owner IS TRUE))
+ ORDER BY t.name;`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+
+ for (const team of result.rows) {
+ team.names = this.createTagList(team?.team_members);
+ team.names.map((a: any) => a.color_code = getColor(a.name));
+ }
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getProjectsByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { id, includeArchived } = req.query;
+ const q = `SELECT p.name,
+ sps.name AS status,
+ sps.icon AS status_icon,
+ sps.color_code AS status_color,
+ p.color_code,
+ end_date AS due_date,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id FROM tasks WHERE project_id = p.id))::INT AS total_spent,
+ (SELECT SUM(total_minutes * 60) FROM tasks WHERE project_id = p.id)::INT AS total_estimated,
+ (SELECT get_project_members(p.id)) AS project_members,
+ (SELECT (SELECT CASE
+ WHEN (end_date::DATE < NOW()::DATE AND p.status_id NOT IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name IN ('Completed', 'Cancelled')))
+ THEN NOW()::DATE - end_date::DATE
+ ELSE 0 END)) AS overdue
+
+ FROM projects p
+ LEFT JOIN sys_project_statuses sps ON p.status_id = sps.id
+ WHERE team_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id
+ AND user_id = $3) END
+ ORDER BY p.name`;
+ const result = await db.query(q, [id, includeArchived, req.user?.id]);
+
+ for (const project of result.rows) {
+ project.total_spent_string = formatDuration(moment.duration(project.total_spent || "0", "seconds"));
+ project.total_allocation = formatDuration(moment.duration(project.total_estimated || "0", "seconds"));
+
+ const overlogged = project.total_estimated - project.total_spent;
+ project.overlogged = formatDuration(moment.duration((overlogged < 0 ? overlogged : 0) || "0", "seconds"));
+ project.project_member_names = this.createTagList(project?.project_members);
+ project.project_member_names.map((a: any) => a.color_code = getColor(a.name));
+ }
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getEstimatedVsLogged(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT t.id,
+ t.name,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE team_id = t.id
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = $1) END)))::INT AS total_logged,
+ (SELECT SUM(total_minutes * 60)
+ FROM tasks
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE team_id = t.id
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = $1) END))::INT AS total_estimated
+ FROM teams t
+ LEFT JOIN team_members tm ON t.id = tm.team_id
+ WHERE tm.user_id = $1
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE))
+ ORDER BY name;`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+ const total_logged: { name: string, y: number }[] = [];
+ const total_estimated: { name: string, y: number }[] = [];
+
+ result.rows.forEach((element: { name: string; total_logged: number; total_estimated: number; }) => {
+ total_logged.push({ name: element.name, y: element.total_logged || 0 });
+ total_estimated.push({ name: element.name, y: element.total_estimated || 0 });
+
+ });
+ return res.status(200).send(new ServerResponse(true, { total_estimated, total_logged }));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { id, includeArchived } = req.query;
+ const q = `SELECT p.name,
+ sps.name,
+ sps.icon,
+ sps.color_code AS status_color,
+ p.color_code,
+ end_date AS due_date,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id FROM tasks WHERE project_id = p.id)),
+ (SELECT SUM(total_minutes * 60) FROM tasks WHERE project_id = p.id)
+ FROM projects p
+ LEFT JOIN sys_project_statuses sps ON p.status_id = sps.id
+ WHERE team_id = $1
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id
+ AND user_id = $3) END`;
+ const result = await db.query(q, [id, includeArchived, req.user?.id]);
+
+ for (const team of result.rows) {
+ team.names = this.createTagList(team?.team_members);
+ team.names.map((a: any) => a.color_code = getColor(a.name));
+ }
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const projectIds = (req.body.projects || []) as string[];
+
+ if (!teamIds || !projectIds.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const q = `SELECT id, (SELECT name)
+ FROM users
+ WHERE id IN (SELECT user_id
+ FROM team_members
+ WHERE team_id IN (${teamIds}))
+ GROUP BY id
+ ORDER BY name`;
+ const result = await db.query(q, []);
+
+ const users = result.rows;
+ const userIds = users.map((u: any) => u.id);
+
+ const projects = await ReportingController.getTimeLoggesByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"));
+
+ return res.status(200).send(new ServerResponse(true, { users, projects }));
+ }
+
+ @HandleExceptions()
+ public static async getMyTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT team_id AS id, name
+ FROM team_members tm
+ LEFT JOIN teams ON teams.id = tm.team_id
+ WHERE tm.user_id = $1
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE))
+ ORDER BY name;`;
+ const result = await db.query(q, [req.user?.id]);
+ result.rows.forEach((team: any) => team.selected = true);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getCategoriesByTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const selectedTeams = (req.body || []) as string[]; // ids
+
+ const ids = selectedTeams.map(id => `'${id}'`).join(",");
+
+ if (!ids)
+ return res.status(200).send(new ServerResponse(true, []));
+
+ const q = `SELECT id, name, color_code FROM project_categories WHERE team_id IN (${ids}) ORDER BY name`;
+ const result = await db.query(q, []);
+ result.rows.forEach((team: any) => team.selected = true);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+
+ @HandleExceptions()
+ public static async getProjectsByTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const selectedTeams = (req.body.selectedTeams || []) as string[];
+ const selectedCategories = (req.body.selectedCategories || []) as string[];
+ const isNoCategorySelected = req.body.noCategoryIncluded;
+
+ const ids = selectedTeams.map(id => `'${id}'`).join(",");
+ const categories = selectedCategories.map(id => `'${id}'`).join(",");
+
+ let categoryQ = "";
+ let noCategoryQ = "";
+
+ if (!ids || (!categories && !isNoCategorySelected))
+ return res.status(200).send(new ServerResponse(true, []));
+
+ if (categories && isNoCategorySelected) {
+ categoryQ = `AND (category_id IS NULL OR category_id IN (${categories}))`;
+ } else if (!categories && isNoCategorySelected) {
+ noCategoryQ = `AND category_id IS NULL`;
+ } else if (categories && !isNoCategorySelected) {
+ categoryQ = `AND category_id IN (${categories})`;
+ }
+
+ // if (categories)
+ // categoryQ = `AND category_id IN (${categories})`;
+
+ // if (isNoCategorySelected === true)
+ // noCategoryQ = `OR (team_id IN (${ids}) AND category_id IS NULL)`;
+
+ const q = `SELECT id, name
+ FROM projects
+ WHERE team_id IN (${ids})
+ ${categoryQ}
+ ${noCategoryQ}
+ ORDER BY name;`;
+ const result = await db.query(q, []);
+ result.rows.forEach((team: any) => team.selected = true);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ public static async getReportingTeamsExport(userId: string, include_archived: boolean) {
+ if (!userId) return [];
+
+ const q = `
+ SELECT t.id,
+ t.name,
+ (SELECT COUNT(*)
+ FROM projects
+ WHERE projects.team_id = tm.team_id
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT 1
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = $1) END) AS projects_count,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT COALESCE(u.name, (SELECT name
+ FROM email_invitations
+ WHERE team_member_id = team_members.id)),
+ avatar_url
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ WHERE team_id = t.id) rec) AS team_members,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT p.name,
+ sps.name AS status,
+ end_date AS due_date,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_id IN (SELECT id FROM tasks WHERE project_id = p.id))::INT AS total_spent,
+ (SELECT SUM(total_minutes * 60) FROM tasks WHERE project_id = p.id)::INT AS total_estimated,
+ (SELECT get_project_members(p.id)) AS project_members,
+ (SELECT (SELECT CASE
+ WHEN (end_date::DATE < NOW()::DATE AND p.status_id NOT IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name IN ('Completed', 'Cancelled')))
+ THEN NOW()::DATE - end_date::DATE
+ ELSE 0 END)) AS overdue
+
+ FROM projects p
+ LEFT JOIN sys_project_statuses sps ON p.status_id = sps.id
+ WHERE team_id = t.id
+ AND (CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT 1
+ FROM archived_projects
+ WHERE project_id = p.id
+ AND user_id = $1) END)
+ ORDER BY p.name) rec) AS projects
+ FROM teams t
+ CROSS JOIN team_members tm
+ WHERE tm.team_id = t.id
+ AND tm.user_id = $1
+ AND role_id IN (SELECT id FROM roles WHERE roles.team_id = t.id AND (admin_role IS TRUE OR owner IS TRUE))
+ ORDER BY t.name;
+ `;
+ const result = await db.query(q, [userId, include_archived]);
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async exportOverviewExcel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const includeArchived = req.query.includeArchived == "true";
+
+ const results = await this.getReportingTeamsExport(req.user?.id as string, includeArchived);
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${exportDate} - Reporting Overview`;
+ const workbook = new Excel.Workbook();
+
+ for (const item of results) {
+ const sheet = workbook.addWorksheet(item.name.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, "_"));
+
+ sheet.columns = [
+ { header: "Project", key: "project", width: 30 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Due Date", key: "due_date", width: 20 },
+ { header: "Overdue", key: "overdue", width: 20 },
+ { header: "Total Allocation", key: "total_allocation", width: 20 },
+ { header: "Over Logged Time", key: "over_logged_time", width: 20 },
+ { header: "Members", key: "members", width: 30 },
+ ];
+
+ sheet.getCell("A1").value = `Team : ${item.name}`;
+ sheet.mergeCells("A1:G1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "D9D9D9" }
+ };
+ sheet.getCell("A1").font = {
+ size: 16
+ };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:G2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "F2F2F2" }
+ };
+ sheet.getCell("A2").font = {
+ size: 12
+ };
+
+ sheet.getRow(4).values = [
+ "Project",
+ "Status",
+ "Due Date",
+ "Overdue",
+ "Total Allocation",
+ "Over Logged Time",
+ "Members",
+ ];
+ sheet.getRow(4).font = {
+ bold: true
+ };
+
+ for (const project of item.projects) {
+ const overlogged = project.total_estimated - project.total_spent;
+ const overloggedTime = formatDuration(moment.duration((overlogged < 0 ? overlogged : 0) || "0", "seconds"));
+ sheet.addRow({
+ project: project.name,
+ status: project.status,
+ due_date: project.due_date ? moment(project.due_date).format("MMM DD,YYYY") : "-",
+ overdue: project.overdue ? `${project.overdue} days` : "-",
+ total_allocation: formatDuration(moment.duration(project.total_estimated || "0", "seconds")),
+ over_logged_time: overloggedTime,
+ members: this.getMembers(project)
+ });
+ }
+
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ private static getMembers(project: any) {
+ return project.project_members.length > 0 ? project.project_members.map((member: {
+ name: any;
+ }) => member.name).join(", ") : "-";
+ }
+
+ public static async getAllocationExport(include_archived: string, teamIds: string, projectIds: string[], duration: string, date_range: string[]) {
+
+ if (!teamIds || !projectIds.length)
+ return { users: [], projects: [] };
+
+ const q = `SELECT id, (SELECT name)
+ FROM users
+ WHERE id IN (SELECT user_id
+ FROM team_members
+ WHERE team_id IN (${teamIds}))
+ GROUP BY id
+ ORDER BY name`;
+ const result = await db.query(q, []);
+
+ const users = result.rows;
+ const userIds = users.map((u: any) => u.id);
+
+ const projects = await ReportingController.getTimeLoggesByProjects(projectIds, userIds, duration, date_range, (include_archived === "true"));
+
+ const dataReturn = { users, projects };
+
+ return dataReturn;
+
+ }
+
+ @HandleExceptions()
+ public static async exportAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const { includeArchived } = req.query;
+ const teams = (req.query.teams as string)?.split(",");
+ const teamIds = teams.map(t => `'${t}'`).join(",");
+ const projectIds = (req.query.projects as string)?.split(",");
+ const { duration } = req.query;
+ const dateRange = (req.query.date_range as string)?.split(",");
+
+ const results = await this.getAllocationExport(includeArchived as string, teamIds, projectIds, duration as string, dateRange);
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${exportDate} - Reporting Allocation`;
+ const workbook = new Excel.Workbook();
+ const sheet = workbook.addWorksheet("Allocation");
+
+ sheet.columns = [
+ { header: "Project", key: "project", width: 25 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ { header: "Total", key: "total", width: 25 },
+ ];
+
+ sheet.getCell("A1").value = `Allocation`;
+ sheet.mergeCells("A1:G1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "D9D9D9" }
+ };
+ sheet.getCell("A1").font = {
+ size: 16
+ };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:G2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "F2F2F2" }
+ };
+ sheet.getCell("A2").font = {
+ size: 12
+ };
+
+ if (results.projects.length > 0) {
+ const rowTop = sheet.getRow(4);
+ rowTop.getCell(1).value = "";
+
+ results.users.forEach((user: any, index: any) => {
+ rowTop.getCell(index + 2).value = user.name;
+ });
+
+ rowTop.getCell(results.users.length + 2).value = "Total";
+
+ // rowTop.getCell(results.users.length + 2).fill = {
+ // type: "pattern",
+ // pattern: "solid",
+ // fgColor: { argb: "F8F7F9" }
+ // };
+
+ rowTop.font = {
+ bold: true
+ };
+
+ for (const project of results.projects) {
+
+ const rowValues = [];
+ rowValues[1] = project.name;
+ project.time_logs.forEach((log: any, index: any) => {
+ rowValues[index + 2] = log.time_logged === "0h 0m 0s" ? "-" : log.time_logged;
+ });
+ rowValues[project.time_logs.length + 2] = project.total;
+ sheet.addRow(rowValues);
+
+ const { lastRow } = sheet;
+ if (lastRow) {
+ const totalCell = lastRow.getCell(project.time_logs.length + 2);
+ totalCell.style.font = { bold: true };
+ }
+
+ }
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+
+ @HandleExceptions()
+ public static async getActiveProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT id,
+ name,
+ updated_at,
+ end_date,
+ (SELECT name FROM sys_project_statuses WHERE status_id = sys_project_statuses.id) AS status
+ FROM projects
+ WHERE team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = $1
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id
+ AND user_id = $1) END
+ AND status_id IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name NOT IN ('Completed', 'Cancelled'))
+ ORDER BY updated_at DESC
+ LIMIT 10`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+
+ for (const project of result.rows) {
+ project.updated_at_string = moment(project.updated_at).fromNow();
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getOverdueProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT id,
+ name,
+ updated_at,
+ end_date,
+ (SELECT name FROM sys_project_statuses WHERE status_id = sys_project_statuses.id) AS status,
+ (SELECT SUM(time_spent)
+ FROM tasks t
+ RIGHT JOIN task_work_log twl ON t.id = twl.task_id
+ WHERE project_id = p.id
+ AND end_date::DATE < twl.created_at::DATE) AS overlogged_hours
+ FROM projects p
+ WHERE team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = $1
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND end_date::DATE < CURRENT_DATE::DATE
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS(SELECT project_id
+ FROM archived_projects
+ WHERE project_id = p.id
+ AND user_id = $1) END
+ AND status_id IN (SELECT ID
+ FROM sys_project_statuses
+ WHERE sys_project_statuses.name NOT IN ('Completed', 'Cancelled'))
+ ORDER BY updated_at DESC
+ LIMIT 10;`;
+ const result = await db.query(q, [req.user?.id, includeArchived]);
+
+ for (const project of result.rows) {
+ project.overlogged_hours = formatDuration(moment.duration(project.overlogged_hours, "seconds"));
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getReportingCustom(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const { searchQuery, size, offset } = this.toPaginationOptions(req.query, "name");
+
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const status = (req.body.status || []) as string[];
+ const statusIds = status.map(p => `'${p}'`).join(",");
+
+ if (!teams.length || !status.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const q = `SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT id,
+ name,
+ (SELECT name FROM sys_project_statuses WHERE id = status_id) AS status,
+ (SELECT color_code FROM sys_project_statuses WHERE id = status_id) AS status_color,
+ (SELECT icon FROM sys_project_statuses WHERE id = status_id) AS status_icon,
+ (SELECT name FROM teams WHERE team_id = teams.id) AS team_name,
+ color_code,
+ start_date,
+ end_date,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE))) AS is_doing_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))) AS is_todo_tasks_count,
+ (SELECT COUNT(*) FROM project_members WHERE project_id = projects.id) AS members_count,
+ (SELECT get_project_members(projects.id)) AS names,
+ (SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
+ (SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
+ (SELECT CASE
+ WHEN ((SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) > updated_at)
+ THEN (SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id)
+ ELSE updated_at END) AS updated_at
+ FROM projects
+ WHERE team_id IN (${teamIds}) ${searchQuery}
+ AND status_id IN (${statusIds})
+ AND NOT EXISTS (SELECT user_id
+ FROM archived_projects
+ WHERE user_id = $1
+ AND project_id = projects.id)
+ AND CASE
+ WHEN ($4 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS (SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id) END
+ ORDER BY NAME ASC
+ LIMIT $2 OFFSET $3) t) AS DATA
+ FROM projects
+ WHERE team_id IN (${teamIds}) ${searchQuery}
+ AND status_id IN (${statusIds})
+ AND NOT EXISTS (SELECT user_id
+ FROM archived_projects
+ WHERE user_id = $1
+ AND project_id = projects.id)
+ AND CASE
+ WHEN ($4 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS (SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id)
+ END;`;
+ const result = await db.query(q, [req.user?.id, size, offset, includeArchived]);
+ const [obj] = result.rows;
+
+ for (const project of obj.data) {
+ project.completed_percentage = Math.round((project.completed_tasks_count / project.all_tasks_count) * 100);
+ project.doing_percentage = Math.round((project.is_doing_tasks_count / project.all_tasks_count) * 100);
+ project.todo_percentage = 100 - (project.completed_percentage + project.doing_percentage);
+ }
+ return res.status(200).send(new ServerResponse(true, obj || []));
+ }
+
+ @HandleExceptions()
+ public static async getProjectDetailsById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT name, (SELECT name FROM teams WHERE teams.id = team_id) AS team_name
+ FROM projects
+ WHERE id = $1;`;
+ const result = await db.query(q, [req.params?.id]);
+ const [data] = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data || { name: "", team_name: "" }));
+ }
+
+ public static async getReportingCustomExport(user_id: string, includeArchived: string, teams: string[], status: string[], searchQuery: string) {
+
+ if (!teams.length || !status.length || !user_id)
+ return { users: [], projects: [] };
+
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+ const statusIds = status.map(s => `'${s}'`).join(",");
+
+ const q = `SELECT id,
+ name,
+ (SELECT name FROM sys_project_statuses WHERE id = status_id) AS status,
+ (SELECT color_code FROM sys_project_statuses WHERE id = status_id) AS status_color,
+ (SELECT icon FROM sys_project_statuses WHERE id = status_id) AS status_icon,
+ (SELECT name FROM teams WHERE team_id = teams.id) AS team_name,
+ color_code,
+ start_date,
+ end_date,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE))) AS is_doing_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_todo IS TRUE))) AS is_todo_tasks_count,
+ (SELECT COUNT(*) FROM project_members WHERE project_id = projects.id) AS members_count,
+ (SELECT get_project_members(projects.id)) AS names,
+ (SELECT name FROM clients WHERE id = projects.client_id) AS client_name,
+ (SELECT name FROM users WHERE id = projects.owner_id) AS project_owner,
+ (SELECT CASE
+ WHEN ((SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id) > updated_at)
+ THEN (SELECT MAX(updated_at)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = projects.id)
+ ELSE updated_at END) AS updated_at
+ FROM projects
+ WHERE team_id IN (${teamIds}) ${searchQuery}
+ AND status_id IN (${statusIds})
+ AND NOT EXISTS (SELECT user_id
+ FROM archived_projects
+ WHERE user_id = $1
+ AND project_id = projects.id)
+ AND CASE
+ WHEN ($2 IS TRUE) THEN team_id IS NOT NULL
+ ELSE NOT EXISTS (SELECT project_id
+ FROM archived_projects
+ WHERE project_id = projects.id)
+ END ORDER BY NAME ASC;`;
+
+ const result = await db.query(q, [user_id, includeArchived]);
+ return result.rows;
+
+ }
+
+ @HandleExceptions()
+ public static async exportProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const { includeArchived } = req.query;
+
+ const { searchQuery } = this.toPaginationOptions(req.query, "name");
+
+ const teams = (req.query.teams as string)?.split(",");
+
+ const status = (req.query.status as string)?.split(",");
+
+ const results = await this.getReportingCustomExport(req.user?.id as string, includeArchived as string, teams, status, searchQuery) as any;
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${exportDate} - Reporting Projects`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Projects");
+
+ sheet.columns = [
+ { header: "Name", key: "name", width: 25 },
+ { header: "Client", key: "client", width: 20 },
+ { header: "Status", key: "status", width: 25 },
+ { header: "Team", key: "team", width: 25 },
+ { header: "Progress", key: "progress", width: 25 },
+ { header: "Last Update", key: "updated", width: 25 },
+ { header: "Members", key: "members", width: 25 },
+ ];
+
+ sheet.getCell("A1").value = `Projects`;
+ sheet.mergeCells("A1:G1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "D9D9D9" }
+ };
+ sheet.getCell("A1").font = {
+ size: 16
+ };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:G2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "F2F2F2" }
+ };
+ sheet.getCell("A2").font = {
+ size: 12
+ };
+
+ sheet.getRow(4).values = [
+ "Name",
+ "Client",
+ "Status",
+ "Team",
+ "Progress (%)",
+ "Last Update",
+ "Members",
+ ];
+
+ sheet.getRow(4).font = {
+ bold: true
+ };
+
+ for (const item of results || []) {
+
+ let progressPercentage = "0";
+
+ if (item.all_tasks_count > 0) {
+ progressPercentage = ((item.completed_tasks_count / item.all_tasks_count) * 100).toFixed();
+ }
+
+ sheet.addRow({
+ name: item.name,
+ client: item.client_name ? item.client_name : "-",
+ status: item.status,
+ team: item.team_name,
+ progress: progressPercentage,
+ updated: moment(item.updated_at).format("MMM DD, YYYY"),
+ members: item.members_count,
+ });
+
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async getUnAssignedUsers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT DISTINCT user_id, (SELECT name FROM team_member_info_view tmiv WHERE tmiv.team_member_id = tm.id)
+ FROM team_members tm
+ LEFT JOIN email_invitations ei ON tm.id = ei.team_member_id
+ WHERE tm.team_id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = $1
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))
+ AND team_member_id NOT IN
+ (SELECT team_member_id
+ FROM project_members pm
+ WHERE pm.project_id IN
+ (SELECT id FROM projects WHERE projects.team_id = tm.team_id));`;
+ const result = await db.query(q, [req.user?.id]);
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getMembersWithOverDueTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT DISTINCT tm.user_id,
+ tm.id AS id,
+ tmiv.name,
+ (SELECT COUNT(DISTINCT task_id)
+ FROM tasks_assignees ta
+ CROSS JOIN tasks t
+ WHERE ta.team_member_id = tm.id
+ AND t.id = ta.task_id
+ AND ((t.end_date::DATE < NOW()::DATE) OR
+ ((t.total_minutes * 60) <
+ (SELECT SUM(time_spent)
+ FROM task_work_log twl
+ WHERE twl.task_id = t.id
+ AND tm.user_id = tm.user_id)))
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))::INT AS overdue_tasks,
+ (SELECT COUNT(DISTINCT project_id)
+ FROM tasks_assignees ta
+ CROSS JOIN tasks t
+ WHERE ta.team_member_id = tm.id
+ AND t.id = ta.task_id
+ AND ((t.end_date::DATE < NOW()::DATE) OR
+ ((t.total_minutes * 60) <
+ (SELECT SUM(time_spent)
+ FROM task_work_log twl
+ WHERE twl.task_id = t.id
+ AND tm.user_id = tm.user_id)))
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))::INT AS projects,
+ (SELECT SUM(t.total_minutes * 60) - SUM(twl.time_spent)
+ FROM tasks_assignees ta
+ CROSS JOIN tasks t,
+ task_work_log twl
+ WHERE ta.team_member_id = tm.id
+ AND twl.task_id = t.id
+ AND twl.user_id = tm.user_id
+ AND t.id = ta.task_id
+ AND ((t.end_date::DATE < NOW()::DATE) OR
+ ((t.total_minutes * 60) <
+ (SELECT SUM(time_spent)
+ FROM task_work_log twl
+ WHERE twl.task_id = t.id
+ AND tm.user_id = tm.user_id)))
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))) AS overdue_time
+ FROM team_members tm
+ LEFT JOIN team_member_info_view tmiv ON tm.id = tmiv.team_member_id
+ WHERE tm.team_id = $1
+ AND tm.id IN (SELECT ta.team_member_id
+ FROM tasks_assignees ta
+ LEFT JOIN tasks t
+ ON t.id = ta.task_id
+ WHERE ((t.end_date::DATE < NOW()::DATE) OR
+ ((t.total_minutes * 60) <
+ (SELECT SUM(time_spent)
+ FROM task_work_log twl
+ WHERE twl.task_id = t.id
+ AND tm.user_id = tm.user_id)))
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))
+ ORDER BY tmiv.name;`;
+ const result = await db.query(q, [req.params?.id]);
+ for (const member of result.rows) {
+ member.overdue_time = formatDuration(moment.duration(member.overdue_time || "0", "seconds"));
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getReportingMemberStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+ const q = `SELECT get_reporting_members_stats($1, $2, $3) AS stats;`;
+ const result = await db.query(q, [req.params.id, includeArchived, req.user?.id]);
+ const [data] = result.rows;
+
+ const { total_estimated, total_logged } = data.stats;
+
+ data.stats.progress = data.stats.total_tasks > 0
+ ? ((data.stats.total_tasks_completed / data.stats.total_tasks) * 100).toFixed(0) : 0;
+
+ data.stats.log_progress = data.stats.total_tasks > 0
+ ? ((data.stats.overdue_tasks / data.stats.total_tasks) * 100).toFixed(0) : 0;
+
+ const totalMinutes = moment.duration(total_estimated, "seconds");
+ const totalSeconds = moment.duration(total_logged, "seconds");
+
+ data.stats.total_estimated_hours_string = formatDuration(totalMinutes);
+ data.stats.total_logged_hours_string = formatDuration(totalSeconds);
+
+ data.stats.overlogged_hours = formatDuration(totalMinutes.subtract(totalSeconds));
+
+ return res.status(200).send(new ServerResponse(true, data.stats || {}));
+ }
+
+ @HandleExceptions()
+ public static async getReportingMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { includeArchived } = req.query;
+
+ // recently logged
+ const logged_q = `SELECT get_reporting_member_recently_logged_tasks($1, $2, $3) AS recently_logged_tasks;`;
+ const loggedResult = await db.query(logged_q, [req.params.id, req.user?.id, includeArchived]);
+ const [loggedData] = loggedResult.rows;
+
+ // currently doing tasks
+ const current_q = `SELECT get_reporting_member_current_doing_tasks($1, $2, $3, $4, $5) AS current_tasks;`;
+ const currentResult = await db.query(current_q, [req.params.id, req.user?.id, includeArchived, 10, 0]);
+ const [currentData] = currentResult.rows;
+
+ // overdue tasks
+ const overdue_q = `SELECT get_reporting_member_current_doing_tasks($1, $2, $3, $4, $5) AS overdue_tasks;`;
+ const overdueResult = await db.query(overdue_q, [req.params.id, req.user?.id, includeArchived, 10, 0]);
+ const [overdueData] = overdueResult.rows;
+
+ for (const task of loggedData.recently_logged_tasks) {
+ task.logged_time = formatDuration(moment.duration(task.logged_time, "seconds"));
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ for (const task of currentData.current_tasks.data) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ for (const task of overdueData.overdue_tasks.data) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.overdue = task.end_date ? moment(task.end_date).from(moment()) : "-";
+ }
+
+ return res.status(200).send(new ServerResponse(true,
+ {
+ logged_tasks: loggedData.recently_logged_tasks,
+ current_tasks: currentData.current_tasks.data,
+ overdue_tasks: overdueData.overdue_tasks.data
+ }));
+ }
+
+ @HandleExceptions()
+ public static async getMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { memberId, teamId } = req.query;
+ if (!teamId || !memberId) return res.status(200).send(new ServerResponse(true, []));
+
+ const q = `SELECT p.id,
+ p.name,
+ (SELECT name FROM teams WHERE teams.id = p.team_id) AS team,
+ (SELECT COUNT(*)
+ FROM tasks_assignees ta
+ LEFT JOIN tasks t ON ta.task_id = t.id
+ WHERE ta.team_member_id = pm.team_member_id
+ AND t.project_id = p.id)::INT AS assigned_task_count,
+ (SELECT COUNT(*) FROM tasks WHERE tasks.project_id = pm.project_id)::INT AS total_task_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE tasks.project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND tasks.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)))::INT AS completed_tasks,
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE tasks.project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND tasks.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))::INT AS incompleted_tasks,
+ (SELECT COUNT(*)
+ FROM tasks
+ LEFT JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE tasks.project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND tasks.end_date::DATE < NOW()::DATE
+ AND tasks.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))::INT AS overdue_tasks
+ FROM projects p
+ LEFT JOIN project_members pm ON p.id = pm.project_id
+ WHERE pm.team_member_id = (SELECT id
+ FROM team_members
+ WHERE team_members.team_id = $1
+ AND user_id = (SELECT user_id FROM team_members WHERE team_members.id = $2));`;
+ const result = await db.query(q, [teamId, memberId]);
+
+ for (const project of result.rows) {
+ project.contribution = ((project.assigned_task_count / project.total_task_count) * 100).toFixed(1);
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { memberId, projectId } = req.query;
+ if (!projectId || !memberId) return res.status(200).send(new ServerResponse(true, []));
+
+ const q = `SELECT name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ end_date AS due_date,
+ completed_at AS completed_date,
+ (total_minutes * 60)::INT AS toal_estimated,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE task_work_log.task_id = t.id)::INT AS total_logged,
+ (t.end_date::DATE < NOW()::DATE AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE))) AS is_overdue
+
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta
+ ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1
+ AND t.project_id = $2
+ ORDER BY name;`;
+ const result = await db.query(q, [memberId, projectId]);
+
+ for (const task of result.rows) {
+ task.overlogged_time = (task.toal_estimated && task.total_logged && task.toal_estimated < task.total_logged) ? formatDuration(moment.duration((task.toal_estimated - task.total_logged), "seconds")) : "-";
+ task.toal_estimated = formatDuration(moment.duration(task.toal_estimated, "seconds"));
+ task.total_logged = formatDuration(moment.duration(task.total_logged, "seconds"));
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows || []));
+ }
+
+ @HandleExceptions()
+ public static async getReportingMembersTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { memberId, teamId } = req.query;
+ if (!teamId || !memberId) return res.status(200).send(new ServerResponse(true, []));
+
+ const overloggedQ = `SELECT t.id,
+ t.name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+ (SELECT ABS((total_minutes * 60) -
+ (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id))) AS overlogged_time,
+ (SELECT get_task_assignees(t.id)) AS assignees
+
+ FROM tasks t
+ LEFT JOIN projects p ON t.project_id = p.id
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE p.team_id = $1
+ AND ta.team_member_id = $2
+ AND ((total_minutes * 60) < (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id))
+ ORDER BY name
+ LIMIT 10;`;
+
+ const earlyQ = `SELECT t.id,
+ t.name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ completed_at AS completed_at,
+ t.end_date,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color
+ FROM tasks t
+ LEFT JOIN projects p ON t.project_id = p.id
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE p.team_id = $1
+ AND ta.team_member_id = $2
+ AND t.end_date::DATE > completed_at::DATE
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))
+ ORDER BY name
+ LIMIT 10;`;
+
+ const lateQ = `SELECT t.id,
+ t.name,
+ (SELECT name
+ FROM task_statuses
+ WHERE id = t.status_id) AS status,
+ completed_at AS completed_at,
+ t.end_date,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id =
+ (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color
+ FROM tasks t
+ LEFT JOIN projects p ON t.project_id = p.id
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE p.team_id = $1
+ AND ta.team_member_id = $2
+ AND t.end_date::DATE < completed_at::DATE
+ AND t.status_id IN
+ (SELECT id
+ FROM task_statuses
+ WHERE category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))
+ ORDER BY name
+ LIMIT 10;`;
+
+ const overloggedResult = await db.query(overloggedQ, [teamId, memberId]);
+ const earlyResult = await db.query(earlyQ, [teamId, memberId]);
+ const lateResult = await db.query(lateQ, [teamId, memberId]);
+
+
+ for (const task of overloggedResult.rows) {
+ task.overlogged_time = formatDuration(moment.duration((task.overlogged_time), "seconds"));
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.assignees?.forEach((a: any) => a.color_code = getColor(a.name));
+ task.names = this.createTagList(task.assignees);
+ }
+
+ for (const task of earlyResult.rows) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ for (const task of lateResult.rows) {
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, {
+ overloggedTasks: overloggedResult.rows,
+ earlyTasks: earlyResult.rows,
+ lateTasks: lateResult.rows
+ }));
+ }
+
+ @HandleExceptions()
+ public static async getProjectsByTeamId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT id, name, FALSE AS selected
+ FROM projects
+ WHERE team_id = $1
+ ORDER BY name;`;
+ const result = await db.query(q, [req.params.id]);
+ result.rows.forEach((team: any) => team.selected = true);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ public static async getTeamMemberInsightData(team_id: string | undefined, projects: string, status: string, search: string, duration: string, userId: string | undefined) {
+ const searchQuery = search ? `AND TO_TSVECTOR(tmiv.name || ' ' || tmiv.email || ' ' || u.name) @@ TO_TSQUERY('${search}:*')` : "";
+
+ const q = `SELECT ROW_TO_JSON(rec) AS team_members
+ FROM (SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT team_members.id,
+ (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = team_members.id),
+ u.avatar_url,
+ (u.socket_id IS NOT NULL) AS is_online,
+ (SELECT COUNT(DISTINCT project_id)
+ FROM tasks
+ WHERE id IN (SELECT task_id
+ FROM task_work_log
+ WHERE ${duration}
+ AND task_work_log.user_id = tmiv.user_id
+ AND task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id IN (${projects})
+ AND project_id IN (SELECT id
+ FROM projects
+ WHERE team_id = $1)))) AS projects_count,
+ (SELECT COUNT(DISTINCT task_id)
+ FROM task_work_log
+ WHERE ${duration}
+ AND task_work_log.user_id = tmiv.user_id
+ AND task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id IN (${projects})
+ AND project_id IN (SELECT id
+ FROM projects
+ WHERE team_id = $1))) AS task_count,
+ (SELECT SUM(time_spent)
+ FROM task_work_log
+ WHERE ${duration}
+ AND task_work_log.user_id = tmiv.user_id
+ AND task_id IN (SELECT id
+ FROM tasks
+ WHERE project_id IN (${projects})
+ AND project_id IN (SELECT id
+ FROM projects
+ WHERE team_id = $1))) AS total_logged_time_seconds,
+ (SELECT name FROM job_titles WHERE id = team_members.job_title_id) AS job_title,
+ (SELECT name FROM roles WHERE id = team_members.role_id) AS role_name,
+ EXISTS(SELECT id
+ FROM roles
+ WHERE id = team_members.role_id
+ AND admin_role IS TRUE) AS is_admin,
+ (CASE
+ WHEN team_members.user_id =
+ (SELECT user_id FROM teams WHERE id = $1)
+ THEN TRUE
+ ELSE FALSE END) AS is_owner,
+ (SELECT email
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = team_members.id),
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = team_members.id
+ AND email_invitations.team_id = team_members.team_id) AS pending_invitation,
+ (SELECT (ARRAY(SELECT NAME
+ FROM teams
+ WHERE id IN (SELECT team_id
+ FROM team_members
+ WHERE team_members.user_id = tmiv.user_id)
+ AND id IN (SELECT team_id
+ FROM team_members
+ WHERE user_id = $2
+ AND role_id IN (SELECT id
+ FROM roles
+ WHERE (admin_role IS TRUE OR owner IS TRUE)))))) AS member_teams
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
+ WHERE team_members.team_id = $1
+ ${searchQuery}
+ AND team_members.id IN
+ (SELECT team_member_id FROM project_members WHERE project_id IN (${projects}))
+ AND team_members.id IN (SELECT team_member_id
+ FROM project_members
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE projects.team_id = $1
+ AND status_id IN (${status})))
+ AND EXISTS(SELECT id
+ FROM task_work_log
+ WHERE ${duration}
+ AND task_work_log.user_id = u.id
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE task_id = tasks.id
+ AND tasks.project_id IN (${projects})
+ AND tasks.project_id IN (SELECT id
+ FROM projects
+ WHERE projects.team_id = $1
+ AND status_id IN
+ (${status}))))
+ ORDER BY tmiv.name, tmiv.email, u.name ASC) t) AS team_members
+ FROM team_members
+ LEFT JOIN users u ON team_members.user_id = u.id
+ LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id
+ WHERE team_members.team_id = $1
+ ${searchQuery}
+ AND team_members.id IN
+ (SELECT team_member_id FROM project_members WHERE project_id IN (${projects}))
+ AND team_members.id IN (SELECT team_member_id
+ FROM project_members
+ WHERE project_id IN (SELECT id
+ FROM projects
+ WHERE projects.team_id = $1
+ AND status_id IN (${status})))
+ AND EXISTS(SELECT id
+ FROM task_work_log
+ WHERE ${duration}
+ AND task_work_log.user_id = u.id
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE task_id = tasks.id
+ AND tasks.project_id IN (${projects})
+ AND tasks.project_id IN (SELECT id
+ FROM projects
+ WHERE projects.team_id = $1
+ AND status_id IN
+ (${status}))))) rec;`;
+ const result = await db.query(q, [team_id, userId]);
+
+ const [data] = result.rows;
+
+ return data.team_members;
+ }
+
+ @HandleExceptions()
+ public static async getReportingMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { search, dateRange, projects, status, duration, teamId } = req.body;
+
+ if (!projects.length || !status.length || !duration || !teamId) {
+ return res.status(200).send(new ServerResponse(true, { total: 0, data: [] }));
+ }
+
+ const projectIds = projects.map((p: any) => `'${p}'`).join(",");
+ const statusIds = status.map((u: any) => `'${u}'`).join(",");
+
+ const dateRangeClause = ReportingController.getDateRangeClause(duration || LAST_WEEK, dateRange);
+
+ const result = await this.getTeamMemberInsightData(teamId, projectIds, statusIds, search, dateRangeClause || "", req.user?.id);
+
+ result.team_members.map((a: any) => {
+ a.color_code = getColor(a.name);
+ a.total_logged_time = formatDuration(moment.duration(a.total_logged_time_seconds || "0", "seconds"));
+ });
+
+ result.data = result.team_members;
+ delete result.team_members;
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ public static async getProjectsOfMember(projectIds: string, statusIds: string, dateRangeClause: string | null, memberId: string) {
+ const q = `SELECT p.name,
+ COUNT(*) AS logged_task_count,
+ SUM(time_spent) AS total_logged_time,
+ p.id,
+ color_code,
+ (SELECT name FROM teams WHERE teams.id = p.team_id) AS team
+ FROM task_work_log
+ LEFT JOIN tasks t ON task_work_log.task_id = t.id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE task_work_log.user_id = (SELECT user_id
+ FROM team_members
+ WHERE id = $1
+ AND team_members.team_id = p.team_id)
+ AND p.id IN (${projectIds})
+ AND p.status_id IN (${statusIds})
+ AND ${dateRangeClause}
+ GROUP BY p.id;`;
+
+ const result = await db.query(q, [memberId,]);
+
+ result.rows.forEach((element: { total_logged_time: string; }) => {
+ element.total_logged_time = formatDuration(moment.duration(element.total_logged_time || "0", "seconds"));
+ });
+
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getProjectsByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { dateRange, projects, status, duration, memberId } = req.body;
+
+ const projectIds = projects.map((p: any) => `'${p}'`).join(",");
+ const statusIds = status.map((u: any) => `'${u}'`).join(",");
+
+ const dateRangeClause = ReportingController.getDateRangeClause(duration || LAST_WEEK, dateRange);
+
+ const data = await this.getProjectsOfMember(projectIds, statusIds, dateRangeClause, memberId);
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async exportMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { duration, team, start, end } = req.query;
+
+ const { searchQuery } = this.toPaginationOptions(req.query, "name");
+
+ const projects = (req.query.projects as string)?.split(",");
+ const status = (req.query.status as string)?.split(",");
+ const projectIds = projects.map((p: any) => `'${p}'`).join(",");
+ const statusIds = status.map((u: any) => `'${u}'`).join(",");
+
+ const dateRange: string[] = [];
+ if (start && end) {
+ dateRange.push(start as string, end as string);
+ }
+
+ const dateRangeClause = ReportingController.getDateRangeClause(duration as string || LAST_WEEK, dateRange);
+
+ const data = await this.getTeamMemberInsightData(team as string, projectIds, statusIds, searchQuery, dateRangeClause || "", req.user?.id);
+
+ for (const teamMember of data.team_members) {
+ teamMember.color_code = getColor(teamMember.name);
+ teamMember.total_logged_time = formatDuration(moment.duration(teamMember.total_logged_time_seconds || "0", "seconds"));
+ teamMember.projects = await this.getProjectsOfMember(projectIds, statusIds, dateRangeClause, teamMember.id);
+ }
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `Team Member Reporting - ${exportDate} `;
+ const workbook = new Excel.Workbook();
+
+ for (const item of data.team_members) {
+ const sheet = workbook.addWorksheet(item.name.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, "_"));
+
+ sheet.columns = [
+ { header: "Project", key: "project", width: 30 },
+ { header: "Team", key: "team", width: 20 },
+ { header: "Logged Task Count", key: "task_count", width: 20 },
+ { header: "Total Logged Time", key: "logged_time", width: 20 },
+ ];
+
+ sheet.getCell("A1").value = `Team Member : ${item.name}`;
+ sheet.mergeCells("A1:D1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "D9D9D9" }
+ };
+ sheet.getCell("A1").font = {
+ size: 16
+ };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:D2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = {
+ type: "pattern",
+ pattern: "solid",
+ fgColor: { argb: "F2F2F2" }
+ };
+ sheet.getCell("A2").font = {
+ size: 12
+ };
+
+ const start = dateRange[0] ? moment(dateRange[0]).format("YYYY-MM-DD") : "-";
+ const end = dateRange[1] ? moment(dateRange[1]).format("YYYY-MM-DD") : "-";
+
+ sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ sheet.mergeCells("A3:D3");
+
+ sheet.getRow(4).values = [
+ "Project",
+ "Team",
+ "Logged Task Count",
+ "Total Logged Time"
+ ];
+ sheet.getRow(4).font = {
+ bold: true
+ };
+
+ for (const project of item.projects) {
+ sheet.addRow({
+ project: project.name,
+ team: project.team,
+ task_count: project.logged_task_count,
+ logged_time: project.total_logged_time
+ });
+ }
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async getHoursLoggedForProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT twl.user_id,
+ (SELECT SUM(time_spent)) AS logged_time,
+ (SELECT name FROM users WHERE id = twl.user_id)
+ FROM task_work_log twl
+ LEFT JOIN tasks t ON twl.task_id = t.id
+ LEFT JOIN projects p ON t.project_id = p.id
+ WHERE p.id = $1
+ GROUP BY twl.user_id;`;
+ const result = await db.query(q, [req.params?.id]);
+
+ const obj: string[] = [];
+ const names: string[] = [];
+
+ for (const member of result.rows) {
+ const memberLog: any = [];
+ memberLog.push(member.name, member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0);
+ names.push(member.name);
+ obj.push(memberLog);
+ }
+
+ return res.status(200).send(new ServerResponse(true, { names, data: obj }));
+ }
+}
diff --git a/worklenz-backend/src/controllers/reporting/interfaces.ts b/worklenz-backend/src/controllers/reporting/interfaces.ts
new file mode 100644
index 00000000..b9616c50
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/interfaces.ts
@@ -0,0 +1,60 @@
+import { IChartObject } from "./overview/reporting-overview-base";
+
+export interface IDuration {
+ label: string;
+ key: string;
+}
+
+export interface IReportingInfo {
+ organization_name: string;
+}
+
+export interface ITeamStatistics {
+ count: number;
+ projects: number;
+ members: number;
+}
+
+export interface IProjectStatistics {
+ count: number;
+ active: number;
+ overdue: number;
+}
+
+export interface IMemberStatistics {
+ count: number;
+ unassigned: number;
+ overdue: number;
+}
+
+export interface IOverviewStatistics {
+ teams: ITeamStatistics;
+ projects: IProjectStatistics;
+ members: IMemberStatistics;
+}
+
+export interface IChartData {
+ chart: IChartObject[];
+}
+
+export interface ITasksByStatus extends IChartData {
+ all: number;
+ todo: number;
+ doing: number;
+ done: number;
+}
+
+export interface ITasksByPriority extends IChartData {
+ all: number;
+ low: number;
+ medium: number;
+ high: number;
+}
+
+export interface ITasksByDue extends IChartData {
+ all: number;
+ completed: number;
+ upcoming: number;
+ overdue: number;
+ no_due: number;
+}
diff --git a/worklenz-backend/src/controllers/reporting/overview/reporting-overview-base.ts b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-base.ts
new file mode 100644
index 00000000..68237fbf
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-base.ts
@@ -0,0 +1,993 @@
+import db from "../../../config/db";
+import { ITasksByDue, ITasksByPriority, ITasksByStatus } from "../interfaces";
+import ReportingControllerBase from "../reporting-controller-base";
+import {
+ DATE_RANGES,
+ TASK_DUE_COMPLETED_COLOR,
+ TASK_DUE_NO_DUE_COLOR,
+ TASK_DUE_OVERDUE_COLOR,
+ TASK_DUE_UPCOMING_COLOR,
+ TASK_PRIORITY_HIGH_COLOR,
+ TASK_PRIORITY_LOW_COLOR,
+ TASK_PRIORITY_MEDIUM_COLOR,
+ TASK_STATUS_DOING_COLOR,
+ TASK_STATUS_DONE_COLOR,
+ TASK_STATUS_TODO_COLOR
+} from "../../../shared/constants";
+import { formatDuration, int } from "../../../shared/utils";
+import moment from "moment";
+
+export interface IChartObject {
+ name: string,
+ color: string,
+ y: number
+}
+
+export default class ReportingOverviewBase extends ReportingControllerBase {
+
+ private static createChartObject(name: string, color: string, y: number) {
+ return {
+ name,
+ color,
+ y
+ };
+ }
+
+ protected static async getTeamsCounts(teamId: string | null, archivedQuery = "") {
+
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'teams', (SELECT COUNT(*) FROM teams WHERE in_organization(id, $1)),
+ 'projects',
+ (SELECT COUNT(*) FROM projects WHERE in_organization(team_id, $1) ${archivedQuery}),
+ 'team_members', (SELECT COUNT(DISTINCT email)
+ FROM team_member_info_view
+ WHERE in_organization(team_id, $1))
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [teamId]);
+ const [data] = res.rows;
+
+ return {
+ count: int(data?.counts.teams),
+ projects: int(data?.counts.projects),
+ members: int(data?.counts.team_members),
+ };
+ }
+
+ protected static async getProjectsCounts(teamId: string | null, archivedQuery = "") {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'active_projects', (SELECT COUNT(*)
+ FROM projects
+ WHERE in_organization(team_id, $1) AND (end_date > CURRENT_TIMESTAMP
+ OR end_date IS NULL) ${archivedQuery}),
+ 'overdue_projects', (SELECT COUNT(*)
+ FROM projects
+ WHERE in_organization(team_id, $1)
+ AND end_date < CURRENT_TIMESTAMP
+ AND status_id NOT IN
+ (SELECT id FROM sys_project_statuses WHERE name = 'Completed') ${archivedQuery})
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [teamId]);
+ const [data] = res.rows;
+
+ return {
+ count: 0,
+ active: int(data?.counts.active_projects),
+ overdue: int(data?.counts.overdue_projects),
+ };
+ }
+
+ protected static async getMemberCounts(teamId: string | null) {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'unassigned', (SELECT COUNT(*)
+ FROM team_members
+ WHERE in_organization(team_id, $1)
+ AND id NOT IN (SELECT team_member_id FROM tasks_assignees)),
+ 'with_overdue', (SELECT COUNT(*)
+ FROM team_members
+ WHERE in_organization(team_id, $1)
+ AND id IN (SELECT team_member_id
+ FROM tasks_assignees
+ WHERE is_overdue(task_id) IS TRUE))
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [teamId]);
+ const [data] = res.rows;
+
+ return {
+ count: 0,
+ unassigned: int(data?.counts.unassigned),
+ overdue: int(data?.counts.with_overdue),
+ };
+ }
+
+ protected static async getProjectStats(projectId: string | null) {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'completed', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_completed(tasks.status_id, tasks.project_id) IS TRUE),
+ 'incompleted', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_completed(tasks.status_id, tasks.project_id) IS FALSE),
+ 'overdue', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_overdue(tasks.id)),
+ 'total_allocated', (SELECT SUM(total_minutes)
+ FROM tasks
+ WHERE project_id = $1),
+ 'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id))
+ FROM tasks
+ WHERE project_id = $1)
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [projectId]);
+ const [data] = res.rows;
+
+ return {
+ completed: int(data?.counts.completed),
+ incompleted: int(data?.counts.incompleted),
+ overdue: int(data?.counts.overdue),
+ total_allocated: moment.duration(int(data?.counts.total_allocated), "minutes").asHours().toFixed(0),
+ total_logged: moment.duration(int(data?.counts.total_logged), "seconds").asHours().toFixed(0),
+ };
+ }
+
+ protected static async getTasksByStatus(projectId: string | null): Promise {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'all', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1),
+ 'todo', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_todo(tasks.status_id, tasks.project_id) IS TRUE),
+ 'doing', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_doing(tasks.status_id, tasks.project_id) IS TRUE),
+ 'done', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND is_completed(tasks.status_id, tasks.project_id) IS TRUE)
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [projectId]);
+ const [data] = res.rows;
+
+ const all = int(data?.counts.all);
+ const todo = int(data?.counts.todo);
+ const doing = int(data?.counts.doing);
+ const done = int(data?.counts.done);
+
+ const chart: IChartObject[] = [];
+
+ return {
+ all,
+ todo,
+ doing,
+ done,
+ chart
+ };
+ }
+
+ protected static async getTasksByPriority(projectId: string | null): Promise {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'low', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND priority_id = (SELECT id FROM task_priorities WHERE value = 0)),
+ 'medium', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND priority_id = (SELECT id FROM task_priorities WHERE value = 1)),
+ 'high', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND priority_id = (SELECT id FROM task_priorities WHERE value = 2))
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [projectId]);
+ const [data] = res.rows;
+
+ const low = int(data?.counts.low);
+ const medium = int(data?.counts.medium);
+ const high = int(data?.counts.high);
+
+ const chart: IChartObject[] = [];
+
+ return {
+ all: 0,
+ low,
+ medium,
+ high,
+ chart
+ };
+ }
+
+ protected static async getTaskCountsByDue(projectId: string | null): Promise {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'no_due', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND end_date IS NULL),
+ 'upcoming', (SELECT COUNT(*)
+ FROM tasks
+ WHERE project_id = $1
+ AND end_date > CURRENT_TIMESTAMP)
+ ) AS counts;
+ `;
+
+ const res = await db.query(q, [projectId]);
+ const [data] = res.rows;
+
+ const chart: IChartObject[] = [];
+
+ return {
+ all: 0,
+ completed: 0,
+ upcoming: int(data?.counts.upcoming),
+ overdue: 0,
+ no_due: int(data?.counts.no_due),
+ chart
+ };
+ }
+
+ protected static createByStatusChartData(body: ITasksByStatus) {
+ body.chart = [
+ this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, body.todo),
+ this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, body.doing),
+ this.createChartObject("Done", TASK_STATUS_DONE_COLOR, body.done),
+ ];
+ }
+
+ protected static createByPriorityChartData(body: ITasksByPriority) {
+ body.chart = [
+ this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, body.low),
+ this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, body.medium),
+ this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, body.high),
+ ];
+ }
+
+ protected static createByDueDateChartData(body: ITasksByDue) {
+ body.chart = [
+ this.createChartObject("Completed", TASK_DUE_COMPLETED_COLOR, body.completed),
+ this.createChartObject("Upcoming", TASK_DUE_UPCOMING_COLOR, body.upcoming),
+ this.createChartObject("Overdue", TASK_DUE_OVERDUE_COLOR, body.overdue),
+ this.createChartObject("No due date", TASK_DUE_NO_DUE_COLOR, body.no_due),
+ ];
+ }
+
+ // Team Member Overview
+
+ protected static async getProjectCountOfTeamMember(teamMemberId: string | null, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND archived_projects.user_id = '${userId}')`;
+
+
+ const q = `
+ SELECT COUNT(*)
+ FROM project_members pm
+ WHERE team_member_id = $1 ${archivedClause};
+ `;
+ const result = await db.query(q, [teamMemberId]);
+ const [data] = result.rows;
+ return int(data.count);
+ }
+
+ protected static async getTeamCountOfTeamMember(teamMemberId: string | null) {
+ const q = `
+ SELECT COUNT(*)
+ FROM team_members
+ WHERE id = $1;
+ `;
+ const result = await db.query(q, [teamMemberId]);
+ const [data] = result.rows;
+ return int(data.count);
+ }
+
+ protected static memberTasksDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+
+ }
+
+ protected static activityLogDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ return `
+ AND (is_doing(
+ (SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
+ )::UUID, t.project_id)
+ OR is_todo(
+ (SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= '${end}'::DATE ORDER BY tl.created_at DESC LIMIT 1
+ )::UUID, t.project_id)
+ OR is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE))`;
+ }
+ return `AND (is_doing(
+ (SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
+ )::UUID, t.project_id)
+ OR is_todo(
+ (SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' AND tl.created_at::DATE <= NOW()::DATE ORDER BY tl.created_at DESC LIMIT 1
+ )::UUID, t.project_id)
+ OR is_completed(t.status_id::UUID, t.project_id::UUID))`;
+ }
+
+ protected static memberAssignDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND ta.updated_at::DATE = '${start}'::DATE`;
+ }
+
+ return `AND ta.updated_at::DATE >= '${start}'::DATE AND ta.updated_at::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static completedDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND t.completed_at::DATE = '${start}'::DATE`;
+ }
+
+ return `AND t.completed_at::DATE >= '${start}'::DATE AND t.completed_at::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static overdueTasksByDate(key: string, dateRange: string[], archivedClause: string) {
+ if (dateRange.length === 2) {
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, '${end}'::DATE) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause})`;
+ }
+
+ return `(SELECT COUNT(CASE WHEN is_overdue_for_date(t.id, NOW()::DATE) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause})`;
+
+ }
+
+
+ protected static overdueTasksDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.ALL_TIME)
+ return `AND t.end_date::DATE < NOW()::DATE`;
+
+ return "";
+ }
+
+ protected static taskWorklogDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND created_at::DATE >= '${start}'::DATE AND created_at::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND created_at::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND created_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND created_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static async getTeamMemberStats(teamMemberId: string | null, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+ const q = `SELECT JSON_BUILD_OBJECT(
+
+ 'total_tasks', (SELECT COUNT(ta.task_id)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause}),
+
+ 'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause}),
+
+ 'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause}),
+
+ 'overdue', (SELECT COUNT(CASE WHEN is_overdue(t.id) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause}),
+
+ 'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${archivedClause})) AS total_logged
+
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1)
+ ) AS counts;`;
+
+ const res = await db.query(q, [teamMemberId]);
+ const [data] = res.rows;
+
+ return {
+ teams: 0,
+ projects: 0,
+ completed: int(data?.counts.completed),
+ ongoing: int(data?.counts.ongoing),
+ overdue: int(data?.counts.overdue),
+ total_tasks: int(data?.counts.total_tasks),
+ total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
+ };
+ }
+
+ protected static async getMemberStats(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+ const durationFilter = this.memberTasksDurationFilter(key, dateRange);
+ const workLogDurationFilter = this.taskWorklogDurationFilter(key, dateRange);
+ const assignClause = this.memberAssignDurationFilter(key, dateRange);
+ const completedDurationClasue = this.completedDurationFilter(key, dateRange);
+ const overdueClauseByDate = this.overdueTasksByDate(key, dateRange, archivedClause);
+
+ const q = `SELECT JSON_BUILD_OBJECT(
+
+ 'total_tasks', (SELECT COUNT(ta.task_id)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${durationFilter} ${archivedClause}),
+
+ 'assigned', (SELECT COUNT(ta.task_id)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}),
+
+ 'completed', (SELECT COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${completedDurationClasue} ${archivedClause}),
+
+ 'ongoing', (SELECT COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause}),
+
+ 'overdue', ${overdueClauseByDate},
+
+ 'total_logged', (SELECT SUM((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND user_id = (SELECT user_id FROM team_member_info_view WHERE team_member_info_view.team_member_id = $1) ${workLogDurationFilter} ${archivedClause})) AS total_logged
+
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1)
+ ) AS counts;`;
+
+ const res = await db.query(q, [teamMemberId]);
+ const [data] = res.rows;
+
+ return {
+ teams: 0,
+ projects: 0,
+ assigned: int(data?.counts.assigned),
+ completed: int(data?.counts.completed),
+ ongoing: int(data?.counts.ongoing),
+ overdue: int(data?.counts.overdue),
+ total_tasks: int(data?.counts.total_tasks),
+ total_logged: formatDuration(moment.duration(data?.counts.total_logged, "seconds")),
+ };
+ }
+
+ protected static async getTasksByProjectOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
+
+ const q = `
+ SELECT p.id,
+ p.color_code AS color,
+ p.name AS label,
+ COUNT(t.id) AS count
+ FROM projects p
+ JOIN tasks t ON p.id = t.project_id
+ JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
+ JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
+ WHERE (is_doing(t.status_id, t.project_id)
+ OR is_todo(t.status_id, t.project_id)
+ OR is_completed(t.status_id, t.project_id)) ${archivedClause}
+ GROUP BY p.id, p.name;
+ `;
+ const result = await db.query(q, [teamMemberId]);
+
+ const chart: IChartObject[] = [];
+
+ const total = result.rows.reduce((accumulator: number, current: {
+ count: number
+ }) => accumulator + int(current.count), 0);
+
+ for (const project of result.rows) {
+ project.count = int(project.count);
+ chart.push(this.createChartObject(project.label, project.color, project.count));
+ }
+
+ return { chart, total, data: result.rows };
+ }
+
+ protected static async getTasksByProjectOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND archived_projects.user_id = '${userId}')`;
+
+ const durationFilter = this.memberTasksDurationFilter(key, dateRange);
+ const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
+ const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
+
+ const q = `
+ SELECT p.id,
+ p.color_code AS color,
+ p.name AS label,
+ COUNT(t.id) AS count
+ FROM projects p
+ JOIN tasks t ON p.id = t.project_id
+ JOIN tasks_assignees ta ON t.id = ta.task_id AND ta.team_member_id = $1
+ JOIN project_members pm ON p.id = pm.project_id AND pm.team_member_id = $1
+ WHERE (is_doing(
+ (SELECT new_value
+ FROM task_activity_logs tl
+ WHERE tl.task_id = t.id
+ AND tl.attribute_type = 'status'
+ ${activityLogDateFilter}
+ ORDER BY tl.created_at DESC
+ LIMIT 1)::UUID, t.project_id)
+ OR is_todo(
+ (SELECT new_value
+ FROM task_activity_logs tl
+ WHERE tl.task_id = t.id
+ AND tl.attribute_type = 'status'
+ ${activityLogDateFilter}
+ ORDER BY tl.created_at DESC
+ LIMIT 1)::UUID, t.project_id)
+ OR ${completedDatebetweenClause}) ${archivedClause}
+ GROUP BY p.id, p.name;
+ `;
+ const result = await db.query(q, [teamMemberId]);
+
+ const chart: IChartObject[] = [];
+
+ const total = result.rows.reduce((accumulator: number, current: {
+ count: number
+ }) => accumulator + int(current.count), 0);
+
+ for (const project of result.rows) {
+ project.count = int(project.count);
+ chart.push(this.createChartObject(project.label, project.color, project.count));
+ }
+
+ return { chart, total, data: result.rows };
+ }
+
+ protected static async getTasksByPriorityOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+
+ const q = `
+ SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
+ COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
+ COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
+ FROM tasks t
+ LEFT JOIN task_priorities tp ON t.priority_id = tp.id
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 AND (is_doing(t.status_id, t.project_id)
+ OR is_todo(t.status_id, t.project_id)
+ OR is_completed(t.status_id, t.project_id)) ${archivedClause};
+ `;
+
+ const result = await db.query(q, [teamMemberId]);
+ const [d] = result.rows;
+
+ const total = int(d.low) + int(d.medium) + int(d.high);
+
+ const chart = [
+ this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
+ this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
+ this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
+ ];
+
+ const data = [
+ { label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
+ { label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
+ { label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
+ ];
+
+ return { chart, total, data };
+ }
+
+ protected static async getTasksByPriorityOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+
+ const durationFilter = this.memberTasksDurationFilter(key, dateRange);
+ const activityLogDateFilter = this.getActivityLogsCreationClause(key, dateRange);
+ const completedDatebetweenClause = this.getCompletedBetweenClause(key, dateRange);
+
+ const q = `
+ SELECT COUNT(CASE WHEN tp.value = 0 THEN 1 END) AS low,
+ COUNT(CASE WHEN tp.value = 1 THEN 1 END) AS medium,
+ COUNT(CASE WHEN tp.value = 2 THEN 1 END) AS high
+ FROM tasks t
+ LEFT JOIN task_priorities tp ON t.priority_id = tp.id
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 AND (is_doing(
+ (SELECT new_value
+ FROM task_activity_logs tl
+ WHERE tl.task_id = t.id
+ AND tl.attribute_type = 'status'
+ ${activityLogDateFilter}
+ ORDER BY tl.created_at DESC
+ LIMIT 1)::UUID, t.project_id)
+ OR is_todo(
+ (SELECT new_value
+ FROM task_activity_logs tl
+ WHERE tl.task_id = t.id
+ AND tl.attribute_type = 'status'
+ ${activityLogDateFilter}
+ ORDER BY tl.created_at DESC
+ LIMIT 1)::UUID, t.project_id)
+ OR ${completedDatebetweenClause}) ${archivedClause};
+ `;
+
+ const result = await db.query(q, [teamMemberId]);
+ const [d] = result.rows;
+
+ const total = int(d.low) + int(d.medium) + int(d.high);
+
+ const chart = [
+ this.createChartObject("Low", TASK_PRIORITY_LOW_COLOR, d.low),
+ this.createChartObject("Medium", TASK_PRIORITY_MEDIUM_COLOR, d.medium),
+ this.createChartObject("High", TASK_PRIORITY_HIGH_COLOR, d.high),
+ ];
+
+ const data = [
+ { label: "Low", color: TASK_PRIORITY_LOW_COLOR, count: d.low },
+ { label: "Medium", color: TASK_PRIORITY_MEDIUM_COLOR, count: d.medium },
+ { label: "High", color: TASK_PRIORITY_HIGH_COLOR, count: d.high },
+ ];
+
+ return { chart, total, data };
+ }
+
+ protected static getActivityLogsCreationClause(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND tl.created_at::DATE <= '${end}'::DATE`;
+ }
+ return `AND tl.created_at::DATE <= NOW()::DATE`;
+ }
+
+ protected static getCompletedBetweenClause(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `is_completed_between(t.id::UUID, '${start}'::DATE, '${end}'::DATE)`;
+ }
+ return `is_completed(t.status_id::UUID, t.project_id::UUID)`;
+ }
+
+ protected static async getTasksByStatusOfTeamMemberOverview(teamMemberId: string | null, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+
+ const q = `
+ SELECT COUNT(ta.task_id) AS total,
+ COUNT(CASE WHEN is_todo(t.status_id, t.project_id) IS TRUE THEN 1 END) AS todo,
+ COUNT(CASE WHEN is_doing(t.status_id, t.project_id) IS TRUE THEN 1 END) AS doing,
+ COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END) AS done
+ FROM tasks t
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause};
+ `;
+
+ const res = await db.query(q, [teamMemberId]);
+ const [d] = res.rows;
+
+ const total = int(d.total);
+
+ const chart = [
+ this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
+ this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
+ this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
+ ];
+
+ const data = [
+ { label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
+ { label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
+ { label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
+ ];
+
+
+ return { chart, total, data };
+ }
+
+ protected static async getTasksByStatusOfTeamMember(teamMemberId: string | null, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+
+ const durationFilter = this.memberTasksDurationFilter(key, dateRange);
+ const completedBetweenFilter = this.getCompletedBetweenClause(key, dateRange);
+ const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
+
+ const q = `
+ SELECT COUNT(ta.task_id) AS total,
+ COUNT(CASE WHEN is_todo((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) IS TRUE THEN 1 END) AS todo,
+ COUNT(CASE WHEN is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) IS TRUE THEN 1 END) AS doing,
+ COUNT(CASE WHEN ${completedBetweenFilter} IS TRUE THEN 1 END) AS done
+ FROM tasks t
+ JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${archivedClause};
+ `;
+
+ const res = await db.query(q, [teamMemberId]);
+ const [d] = res.rows;
+
+ const total = int(d.todo) + int(d.doing) + int(d.done);
+
+ const chart = [
+ this.createChartObject("Todo", TASK_STATUS_TODO_COLOR, d.todo),
+ this.createChartObject("Doing", TASK_STATUS_DOING_COLOR, d.doing),
+ this.createChartObject("Done", TASK_STATUS_DONE_COLOR, d.done),
+ ];
+
+ const data = [
+ { label: "Todo", color: TASK_STATUS_TODO_COLOR, count: d.todo },
+ { label: "Doing", color: TASK_STATUS_DOING_COLOR, count: d.doing },
+ { label: "Done", color: TASK_STATUS_DONE_COLOR, count: d.done },
+ ];
+
+ return { chart, total, data };
+ }
+
+ protected static async getProjectsByStatus(teamId: string | null, archivedClause = ""): Promise {
+ const q = `WITH ProjectCounts AS (
+ SELECT
+ COUNT(*) AS all_projects,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Cancelled') THEN 1 ELSE 0 END) AS cancelled,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Blocked') THEN 1 ELSE 0 END) AS blocked,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'On Hold') THEN 1 ELSE 0 END) AS on_hold,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Proposed') THEN 1 ELSE 0 END) AS proposed,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Planning') THEN 1 ELSE 0 END) AS in_planning,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'In Progress') THEN 1 ELSE 0 END) AS in_progress,
+ SUM(CASE WHEN status_id = (SELECT id FROM sys_project_statuses WHERE name = 'Completed') THEN 1 ELSE 0 END) AS completed
+ FROM projects
+ WHERE team_id = $1 ${archivedClause})
+
+ SELECT JSON_BUILD_OBJECT(
+ 'all_projects', all_projects,
+ 'cancelled', cancelled,
+ 'blocked', blocked,
+ 'on_hold', on_hold,
+ 'proposed', proposed,
+ 'in_planning', in_planning,
+ 'in_progress', in_progress,
+ 'completed', completed
+ ) AS counts
+ FROM ProjectCounts;`;
+ const res = await db.query(q, [teamId]);
+ const [data] = res.rows;
+
+ const all = int(data?.counts.all_projects);
+ const cancelled = int(data?.counts.cancelled);
+ const blocked = int(data?.counts.blocked);
+ const on_hold = int(data?.counts.on_hold);
+ const proposed = int(data?.counts.proposed);
+ const in_planning = int(data?.counts.in_planning);
+ const in_progress = int(data?.counts.in_progress);
+ const completed = int(data?.counts.completed);
+
+ const chart : IChartObject[] = [];
+
+ return {
+ all,
+ cancelled,
+ blocked,
+ on_hold,
+ proposed,
+ in_planning,
+ in_progress,
+ completed,
+ chart
+ };
+
+ }
+
+ protected static async getProjectsByCategory(teamId: string | null, archivedClause = ""): Promise {
+ const q = `
+ SELECT
+ pc.id,
+ pc.color_code AS color,
+ pc.name AS label,
+ COUNT(pc.id) AS count
+ FROM project_categories pc
+ JOIN projects ON pc.id = projects.category_id
+ WHERE projects.team_id = $1 ${archivedClause}
+ GROUP BY pc.id, pc.name;
+ `;
+ const result = await db.query(q, [teamId]);
+
+ const chart: IChartObject[] = [];
+
+ const total = result.rows.reduce((accumulator: number, current: {
+ count: number
+ }) => accumulator + int(current.count), 0);
+
+ for (const category of result.rows) {
+ category.count = int(category.count);
+ chart.push({
+ name: category.label,
+ color: category.color,
+ y: category.count
+ });
+ }
+
+ return { chart, total, data: result.rows };
+
+ }
+
+ protected static async getProjectsByHealth(teamId: string | null, archivedClause = ""): Promise {
+ const q = `
+ SELECT JSON_BUILD_OBJECT(
+ 'needs_attention', (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id = $1 ${archivedClause}
+ AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Needs Attention')),
+ 'at_risk', (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id = $1 ${archivedClause}
+ AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'At Risk')),
+ 'good', (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id = $1 ${archivedClause}
+ AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Good')),
+ 'not_set', (SELECT COUNT(*)
+ FROM projects
+ WHERE team_id = $1 ${archivedClause}
+ AND health_id = (SELECT id FROM sys_project_healths WHERE name = 'Not Set'))
+ ) AS counts;
+ `;
+ const res = await db.query(q, [teamId]);
+ const [data] = res.rows;
+
+ const not_set = int(data?.counts.not_set);
+ const needs_attention = int(data?.counts.needs_attention);
+ const at_risk = int(data?.counts.at_risk);
+ const good = int(data?.counts.good);
+
+ const chart: IChartObject[] = [];
+
+ return {
+ not_set,
+ needs_attention,
+ at_risk,
+ good,
+ chart
+ };
+
+ }
+
+ // Team Overview
+ protected static createByProjectStatusChartData(body: any) {
+ body.chart = [
+ this.createChartObject("Cancelled", "#f37070", body.cancelled),
+ this.createChartObject("Blocked", "#cbc8a1", body.blocked),
+ this.createChartObject("On Hold", "#cbc8a1", body.on_hold),
+ this.createChartObject("Proposed", "#cbc8a1", body.proposed),
+ this.createChartObject("In Planning", "#cbc8a1", body.in_planning),
+ this.createChartObject("In Progress", "#80ca79", body.in_progress),
+ this.createChartObject("Completed", "#80ca79", body.completed)
+ ];
+ }
+
+ protected static createByProjectHealthChartData(body: any) {
+ body.chart = [
+ this.createChartObject("Not Set", "#a9a9a9", body.not_set),
+ this.createChartObject("Needs Attention", "#f37070", body.needs_attention),
+ this.createChartObject("At Risk", "#fbc84c", body.at_risk),
+ this.createChartObject("Good", "#75c997", body.good)
+ ];
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.ts b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.ts
new file mode 100644
index 00000000..c6736125
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-controller.ts
@@ -0,0 +1,391 @@
+import HandleExceptions from "../../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
+import { ServerResponse } from "../../../models/server-response";
+import db from "../../../config/db";
+import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
+import ReportingOverviewBase from "./reporting-overview-base";
+import { GroupBy, ITaskGroup } from "../../tasks-controller-base";
+import TasksControllerV2, { TaskListGroup } from "../../tasks-controller-v2";
+import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
+import { ReportingExportModel } from "../../../models/reporting-export";
+import moment from "moment";
+import ReportingControllerBase from "../reporting-controller-base";
+
+export default class ReportingOverviewController extends ReportingOverviewBase {
+ @HandleExceptions()
+ public static async getStatistics(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = this.getCurrentTeamId(req);
+ const includeArchived = req.query.archived === "true";
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${req.user?.id}') `;
+
+ const teams = await this.getTeamsCounts(teamId, archivedClause);
+ const projects = await this.getProjectsCounts(teamId, archivedClause);
+ const members = await this.getMemberCounts(teamId);
+
+ projects.count = teams.projects;
+ members.count = teams.members;
+
+ const body = {
+ teams,
+ projects,
+ members
+ };
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ @HandleExceptions()
+ public static async getTeams(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = this.getCurrentTeamId(req);
+ const includeArchived = req.query.archived === "true";
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND archived_projects.user_id = '${req.user?.id}')`;
+
+ const q = `
+ SELECT id,
+ name,
+ COALESCE((SELECT COUNT(*) FROM projects WHERE team_id = teams.id ${archivedClause}), 0) AS projects_count,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (
+ --
+ SELECT (SELECT name
+ FROM team_member_info_view
+ WHERE team_member_info_view.team_member_id = tm.id),
+ u.avatar_url
+ FROM team_members tm
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE team_id = teams.id
+ --
+ ) rec) AS members
+ FROM teams
+ WHERE in_organization(id, $1)
+ ORDER BY name;
+ `;
+ const result = await db.query(q, [teamId]);
+
+ for (const team of result.rows) {
+ team.members = this.createTagList(team?.members);
+ team.members.map((a: any) => a.color_code = getColor(a.name));
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
+ const archived = req.query.archived === "true";
+
+ const teamId = req.query.team as string;
+
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const teamFilterClause = `p.team_id = $1`;
+
+ const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
+
+
+ for (const project of result.projects) {
+ project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
+ project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
+ project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
+ if (project.days_left && project.is_overdue) {
+ project.days_left = project.days_left.toString().replace(/-/g, "");
+ }
+ project.is_today = this.isToday(project.end_date);
+ project.estimated_time = int(project.estimated_time);
+ project.actual_time = int(project.actual_time);
+ project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
+ project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
+ project.tasks_stat = {
+ todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
+ doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
+ done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
+ };
+ if (project.update.length > 0) {
+ const update = project.update[0];
+ const placeHolders = update.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < update.mentions.length) {
+ update.content = update.content.replace(placeHolder, `
+ @${update.mentions[index].user_name} `);
+ }
+ });
+ }
+ project.comment = update.content;
+ }
+ if (project.last_activity) {
+ if (project.last_activity.attribute_type === "estimation") {
+ project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
+ project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
+ }
+ if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
+ project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
+ project.last_activity.log_text = await formatLogText(project.last_activity);
+ project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
+ project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ @HandleExceptions()
+ public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = req.params.team_id?.trim() || null;
+ const teamMemberId = (req.query.member as string)?.trim() || null;
+ const teamMemberFilter = teamId === "undefined" ? `AND pm.team_member_id = $1` : teamMemberId ? `AND pm.team_member_id = $2` : "";
+ const teamIdFilter = teamId === "undefined" ? "p.team_id IS NOT NULL" : `p.team_id = $1`;
+
+ const q = `
+ SELECT p.id,
+ p.name,
+ p.color_code,
+ p.team_id,
+ p.status_id
+ FROM projects p
+ LEFT JOIN project_members pm ON pm.project_id = p.id
+ WHERE ${teamIdFilter} ${teamMemberFilter}
+ GROUP BY p.id, p.name;`;
+
+ const params = teamId === "undefined" ? [teamMemberId] : teamMemberId ? [teamId, teamMemberId] : [teamId];
+
+ const result = await db.query(q, params);
+
+ const data = result.rows;
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = req.params.team_id?.trim() || null;
+ const archived = req.query.archived === "true";
+
+ const pmArchivedClause = archived ? `` : `AND project_members.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = project_members.project_id AND user_id = '${req.user?.id}')`;
+ const taArchivedClause = archived ? `` : `AND (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE project_id = (SELECT tasks.project_id FROM tasks WHERE tasks.id = tasks_assignees.task_id) AND user_id = '${req.user?.id}')`;
+
+ const q = `
+ SELECT team_member_id AS id,
+ name,
+ email,
+
+ (SELECT COUNT(*)
+ FROM project_members
+ WHERE project_members.team_member_id = team_member_info_view.team_member_id ${pmArchivedClause}) AS projects,
+
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id ${taArchivedClause}) AS tasks,
+
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
+ AND is_overdue(task_id) IS TRUE ${taArchivedClause}) AS overdue,
+
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
+ AND task_id IN (SELECT id
+ FROM tasks
+ WHERE is_completed(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS completed,
+
+ (SELECT COUNT(*)
+ FROM tasks_assignees
+ WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id
+ AND task_id IN (SELECT id
+ FROM tasks
+ WHERE is_doing(tasks.status_id, tasks.project_id)) ${taArchivedClause}) AS ongoing
+
+ FROM team_member_info_view
+ WHERE team_id = $1
+ ORDER BY name;
+ `;
+
+ const result = await db.query(q, [teamId]);
+
+ for (const member of result.rows) {
+ member.projects = int(member.projects);
+ member.tasks = int(member.tasks);
+ member.overdue = int(member.overdue);
+ member.completed = int(member.completed);
+ member.ongoing = int(member.ongoing);
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getProjectOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const projectId = req.params.project_id || null;
+
+ const stats = await this.getProjectStats(projectId);
+ const byStatus = await this.getTasksByStatus(projectId);
+ const byPriority = await this.getTasksByPriority(projectId);
+ const byDue = await this.getTaskCountsByDue(projectId);
+
+ byPriority.all = byStatus.all;
+
+ byDue.all = byStatus.all;
+ byDue.completed = stats.completed;
+ byDue.overdue = stats.overdue;
+
+ const body = {
+ stats,
+ by_status: byStatus,
+ by_priority: byPriority,
+ by_due: byDue
+ };
+
+ this.createByStatusChartData(body.by_status);
+ this.createByPriorityChartData(body.by_priority);
+ this.createByDueDateChartData(body.by_due);
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ @HandleExceptions()
+ public static async getProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const projectId = req.params.project_id?.trim() || null;
+
+ const members = await ReportingExportModel.getProjectMembers(projectId as string);
+
+ return res.status(200).send(new ServerResponse(true, members));
+ }
+
+ @HandleExceptions()
+ public static async getProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const groupBy = (req.query.group || GroupBy.STATUS) as string;
+ const projectId = req.params.project_id?.trim() || null;
+
+ const groups = await TasksControllerV2.getGroups(groupBy, projectId as string);
+ const tasks = await this.getAllTasks(projectId);
+
+ const map = groups.reduce((g: { [x: string]: ITaskGroup }, group) => {
+ if (group.id)
+ g[group.id] = new TaskListGroup(group);
+ return g;
+ }, {});
+
+ TasksControllerV2.updateMapByGroup(tasks, groupBy, map);
+ const updatedGroups = Object.keys(map).map(key => {
+ const group = map[key];
+ if (groupBy === GroupBy.PHASE)
+ group.color_code = getColor(group.name) + TASK_PRIORITY_COLOR_ALPHA;
+ return {
+ id: key,
+ ...group
+ };
+ });
+
+ return res.status(200).send(new ServerResponse(true, updatedGroups));
+ }
+
+ @HandleExceptions()
+ public static async getTeamMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamMemberId = req.query.teamMemberId as string;
+ const archived = req.query.archived === "true";
+
+ const stats = await this.getTeamMemberStats(teamMemberId, archived, req.user?.id as string);
+ const byStatus = await this.getTasksByStatusOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
+ const byProject = await this.getTasksByProjectOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
+ const byPriority = await this.getTasksByPriorityOfTeamMemberOverview(teamMemberId, archived, req.user?.id as string);
+
+ stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
+
+ const body = {
+ stats,
+ by_status: byStatus,
+ by_project: byProject,
+ by_priority: byPriority
+ };
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ @HandleExceptions()
+ public static async getMemberOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamMemberId = req.query.teamMemberId as string;
+ const { duration, date_range } = req.query;
+ const archived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const stats = await this.getMemberStats(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
+ const byStatus = await this.getTasksByStatusOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
+ const byProject = await this.getTasksByProjectOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
+ const byPriority = await this.getTasksByPriorityOfTeamMember(teamMemberId, duration as string, dateRange, archived, req.user?.id as string);
+
+ stats.teams = await this.getTeamCountOfTeamMember(teamMemberId);
+ stats.projects = await this.getProjectCountOfTeamMember(teamMemberId, archived, req.user?.id as string);
+
+ const body = {
+ stats,
+ by_status: byStatus,
+ by_project: byProject,
+ by_priority: byPriority
+ };
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ @HandleExceptions()
+ public static async getMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamMemberId = req.params.team_member_id?.trim() || null;
+ const projectId = (req.query.project as string)?.trim() || null;
+ const onlySingleMember = req.query.only_single_member as string;
+ const { duration, date_range } = req.query;
+ const includeArchived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, onlySingleMember, duration as string, dateRange, includeArchived, req.user?.id as string);
+
+ return res.status(200).send(new ServerResponse(true, results));
+ }
+
+ @HandleExceptions()
+ public static async getTeamOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teamId = req.params.team_id || null;
+ const archived = req.query.archived === "true";
+
+ const archivedClause = await this.getArchivedProjectsClause(archived, req.user?.id as string, "projects.id");
+
+ const byStatus = await this.getProjectsByStatus(teamId, archivedClause);
+ const byCategory = await this.getProjectsByCategory(teamId, archivedClause);
+ const byHealth = await this.getProjectsByHealth(teamId, archivedClause);
+
+ byCategory.all = byStatus.all;
+ byHealth.all = byStatus.all;
+
+ const body = {
+ by_status: byStatus,
+ by_category: byCategory,
+ by_health: byHealth
+ };
+
+ this.createByProjectStatusChartData(body.by_status);
+ this.createByProjectHealthChartData(body.by_health);
+
+ return res.status(200).send(new ServerResponse(true, body));
+
+ }
+
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/overview/reporting-overview-export-controller.ts b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-export-controller.ts
new file mode 100644
index 00000000..fbc56961
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/overview/reporting-overview-export-controller.ts
@@ -0,0 +1,581 @@
+import HandleExceptions from "../../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
+import ReportingOverviewBase from "./reporting-overview-base";
+import { ReportingExportModel } from "../../../models/reporting-export";
+import { formatDuration, formatLogText, getColor, int } from "../../../shared/utils";
+import moment from "moment";
+import Excel from "exceljs";
+import ReportingControllerBase from "../reporting-controller-base";
+import { TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
+
+export default class ReportingOverviewExportController extends ReportingOverviewBase {
+
+ @HandleExceptions()
+ public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
+ const archived = req.query.archived === "true";
+
+ const teamId = req.query.team as string;
+
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const teamFilterClause = `p.team_id = $1`;
+
+ const result = await ReportingControllerBase.getProjectsByTeam(teamId, size, offset, searchQuery, sortField, sortOrder, "", "", "", archivedClause, teamFilterClause, "");
+
+ for (const project of result.projects) {
+ project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
+ project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
+ project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
+ if (project.days_left && project.is_overdue) {
+ project.days_left = project.days_left.toString().replace(/-/g, "");
+ }
+ project.is_today = this.isToday(project.end_date);
+ project.estimated_time = int(project.estimated_time);
+ project.actual_time = int(project.actual_time);
+ project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
+ project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
+ project.tasks_stat = {
+ todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
+ doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
+ done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
+ };
+ if (project.update.length > 0) {
+ const update = project.update[0];
+ const placeHolders = update.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < update.mentions.length) {
+ update.content = update.content.replace(placeHolder, `
+ @${update.mentions[index].user_name} `);
+ }
+ });
+ }
+ project.comment = update.content;
+ }
+ if (project.last_activity) {
+ if (project.last_activity.attribute_type === "estimation") {
+ project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
+ project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
+ }
+ if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
+ project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
+ project.last_activity.log_text = await formatLogText(project.last_activity);
+ project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
+ project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
+ }
+ }
+
+ return result;
+ }
+
+
+
+
+ @HandleExceptions()
+ public static async getProjectsByTeamOrMember(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamId = (req.query.team_id as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || null;
+
+ const result = await ReportingControllerBase.exportProjects(teamId as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} projects - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Projects");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Project", key: "name", width: 30 },
+ { header: "Client", key: "client", width: 20 },
+ { header: "Category", key: "category", width: 20 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Start Date", key: "start_date", width: 20 },
+ { header: "End Date", key: "end_date", width: 20 },
+ { header: "Days Left/Overdue", key: "days_left", width: 20 },
+ { header: "Estimated Hours", key: "estimated_hours", width: 20 },
+ { header: "Actual Hours", key: "actual_hours", width: 20 },
+ { header: "Done Tasks(%)", key: "done_tasks", width: 20 },
+ { header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
+ { header: "Todo Tasks(%)", key: "todo_tasks", width: 20 },
+ { header: "Last Activity", key: "last_activity", width: 20 },
+ { header: "Project Health", key: "project_health", width: 20 },
+ { header: "Project Update", key: "project_update", width: 20 }
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Projects from ${teamName}`;
+ sheet.mergeCells("A1:O1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:O2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Project", "Client", "Category", "Status", "Start Date", "End Date", "Days Left/Overdue", "Estimated Hours", "Actual Hours", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)", "Last Activity", "Project Health", "Project Update"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of result.projects) {
+
+ if (item.is_overdue && item.days_left) {
+ item.days_left = `-${item.days_left}`;
+ }
+
+ if (item.is_today) {
+ item.days_left = `Today`;
+ }
+
+ sheet.addRow({
+ name: item.name,
+ client: item.client ? item.client : "-",
+ category: item.category_name ? item.category_name : "-",
+ status: item.status_name ? item.status_name : "-",
+ start_date: item.start_date ? moment(item.start_date).format("YYYY-MM-DD") : "-",
+ end_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
+ days_left: item.days_left ? item.days_left.toString() : "-",
+ estimated_hours: item.estimated_time ? item.estimated_time.toString() : "-",
+ actual_hours: item.actual_time ? item.actual_time.toString() : "-",
+ done_tasks: item.tasks_stat.done ? `${item.tasks_stat.done}` : "-",
+ doing_tasks: item.tasks_stat.doing ? `${item.tasks_stat.doing}` : "-",
+ todo_tasks: item.tasks_stat.todo ? `${item.tasks_stat.todo}` : "-",
+ last_activity: item.last_activity ? item.last_activity.last_activity_string : "-",
+ project_health: item.project_health,
+ project_update: item.comment ? item.comment : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async getMembersByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamId = (req.query.team_id as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || null;
+
+ const result = await ReportingExportModel.getMembersByTeam(teamId);
+
+ for (const member of result) {
+ member.projects = int(member.projects);
+ member.tasks = int(member.tasks);
+ member.overdue = int(member.overdue);
+ member.completed = int(member.completed);
+ member.ongoing = int(member.ongoing);
+ }
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} members - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Members");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Name", key: "name", width: 30 },
+ { header: "Email", key: "email", width: 20 },
+ { header: "Projects", key: "projects", width: 20 },
+ { header: "Tasks", key: "tasks", width: 20 },
+ { header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
+ { header: "Completed Tasks", key: "completed_tasks", width: 20 },
+ { header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Members from ${teamName}`;
+ sheet.mergeCells("A1:G1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:G2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Name", "Email", "Projects", "Tasks", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of result) {
+ sheet.addRow({
+ name: item.name,
+ email: item.email ? item.email : "-",
+ projects: item.projects ? item.projects.toString() : "-",
+ tasks: item.tasks ? item.tasks.toString() : "-",
+ overdue_tasks: item.overdue ? item.overdue.toString() : "-",
+ completed_tasks: item.completed ? item.completed.toString() : "-",
+ ongoing_tasks: item.ongoing ? item.ongoing.toString() : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async exportProjectMembers(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const projectId = (req.query.project_id as string)?.trim() || null;
+ const projectName = (req.query.project_name as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || "";
+
+ const results = await ReportingExportModel.getProjectMembers(projectId as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} ${projectName} members - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Members");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Name", key: "name", width: 30 },
+ { header: "Tasks Count", key: "tasks_count", width: 20 },
+ { header: "Completed Tasks", key: "completed_tasks", width: 20 },
+ { header: "Incomplete Tasks", key: "incomplete_tasks", width: 20 },
+ { header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
+ { header: "Contribution(%)", key: "contribution", width: 20 },
+ { header: "Progress(%)", key: "progress", width: 20 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Members from ${projectName} - ${teamName}`;
+ sheet.mergeCells("A1:H1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:H2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Name", "Tasks Count", "Completed Tasks", "Incomplete Tasks", "Overdue Tasks", "Contribution(%)", "Progress(%)", "Logged Time"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of results) {
+ sheet.addRow({
+ name: item.name,
+ tasks_count: item.tasks_count ? item.tasks_count : "-",
+ completed_tasks: item.completed ? item.completed : "-",
+ incomplete_tasks: item.incompleted ? item.incompleted : "-",
+ overdue_tasks: item.overdue ? item.overdue : "-",
+ contribution: item.contribution ? item.contribution : "-",
+ progress: item.progress ? item.progress : "-",
+ logged_time: item.time_logged ? item.time_logged : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async exportProjectTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const projectId = (req.query.project_id as string)?.trim() || null;
+ const projectName = (req.query.project_name as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || "";
+
+ const results = await this.getAllTasks(projectId);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} ${projectName} tasks - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Tasks");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Task", key: "task", width: 30 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Priority", key: "priority", width: 20 },
+ { header: "Phase", key: "phase", width: 20 },
+ { header: "Due Date", key: "due_date", width: 20 },
+ { header: "Completed On", key: "completed_on", width: 20 },
+ { header: "Days Overdue", key: "days_overdue", width: 20 },
+ { header: "Estimated Time", key: "estimated_time", width: 20 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ { header: "Overlogged Time", key: "overlogged_time", width: 20 },
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Tasks from ${projectName} - ${teamName}`;
+ sheet.mergeCells("A1:J1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:J2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Task", "Status", "Priority", "Phase", "Due Date", "Completed On", "Days Overdue", "Estimated Time", "Logged Time", "Overlogged Time"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of results) {
+ const time_spent = { hours: ~~(item.total_minutes_spent / 60), minutes: item.total_minutes_spent % 60 };
+ item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
+
+ sheet.addRow({
+ task: item.name,
+ status: item.status_name ? item.status_name : "-",
+ priority: item.priority_name ? item.priority_name : "-",
+ phase: item.phase_name ? item.phase_name : "-",
+ due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
+ completed_on: item.completed_at ? moment(item.completed_at).format("YYYY-MM-DD") : "-",
+ days_overdue: item.overdue_days ? item.overdue_days : "-",
+ estimated_time: item.total_minutes !== "0" ? `${~~(item.total_minutes / 60)}h ${(item.total_minutes % 60)}m` : "-",
+ logged_time: item.total_minutes_spent ? `${time_spent.hours}h ${(time_spent.minutes)}m` : "-",
+ overlogged_time: item.overlogged_time_string !== "0h 0m" ? item.overlogged_time_string : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+ }
+
+ @HandleExceptions()
+ public static async exportMemberTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
+ const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || "";
+
+ const { duration, date_range, only_single_member, archived} = req.query;
+
+ const includeArchived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, null, only_single_member as string, duration as string, dateRange, includeArchived, req.user?.id as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamMemberName} tasks - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Tasks");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Task", key: "task", width: 30 },
+ { header: "Project", key: "project", width: 20 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Priority", key: "priority", width: 20 },
+ { header: "Due Date", key: "due_date", width: 20 },
+ { header: "Completed Date", key: "completed_on", width: 20 },
+ { header: "Estimated Time", key: "estimated_time", width: 20 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ { header: "Overlogged Time", key: "overlogged_time", width: 20 },
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Tasks of ${teamMemberName} - ${teamName}`;
+ sheet.mergeCells("A1:I1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:I2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of results) {
+ sheet.addRow({
+ task: item.name,
+ project: item.project_name ? item.project_name : "-",
+ status: item.status_name ? item.status_name : "-",
+ priority: item.priority_name ? item.priority_name : "-",
+ due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
+ completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
+ estimated_time: item.estimated_string ? item.estimated_string : "-",
+ logged_time: item.time_spent_string ? item.time_spent_string : "-",
+ overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+ @HandleExceptions()
+ public static async exportFlatTasks(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
+ const teamMemberName = (req.query.team_member_name as string)?.trim() || null;
+ const projectId = (req.query.project_id as string)?.trim() || null;
+ const projectName = (req.query.project_name as string)?.trim() || null;
+
+ const includeArchived = req.query.archived === "true";
+
+ const results = await ReportingExportModel.getMemberTasks(teamMemberId as string, projectId, "false", "", [], includeArchived, req.user?.id as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamMemberName}'s tasks in ${projectName} - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Tasks");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Task", key: "task", width: 30 },
+ { header: "Project", key: "project", width: 20 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Priority", key: "priority", width: 20 },
+ { header: "Due Date", key: "due_date", width: 20 },
+ { header: "Completed Date", key: "completed_on", width: 20 },
+ { header: "Estimated Time", key: "estimated_time", width: 20 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ { header: "Overlogged Time", key: "overlogged_time", width: 20 },
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Tasks of ${teamMemberName} in ${projectName}`;
+ sheet.mergeCells("A1:I1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:I2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Task", "Project", "Status", "Priority", "Due Date", "Completed Date", "Estimated Time", "Logged Time", "Overlogged Time"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of results) {
+ sheet.addRow({
+ task: item.name,
+ project: item.project_name ? item.project_name : "-",
+ status: item.status_name ? item.status_name : "-",
+ priority: item.priority_name ? item.priority_name : "-",
+ due_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
+ completed_on: item.completed_date ? moment(item.completed_date).format("YYYY-MM-DD") : "-",
+ estimated_time: item.estimated_string ? item.estimated_string : "-",
+ logged_time: item.time_spent_string ? item.time_spent_string : "-",
+ overlogged_time: item.overlogged_time ? item.overlogged_time : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/projects/reporting-projects-base.ts b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-base.ts
new file mode 100644
index 00000000..ef26e3b7
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-base.ts
@@ -0,0 +1,4 @@
+import ReportingControllerBase from "../reporting-controller-base";
+
+export default class ReportingProjectsBase extends ReportingControllerBase {
+}
diff --git a/worklenz-backend/src/controllers/reporting/projects/reporting-projects-controller.ts b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-controller.ts
new file mode 100644
index 00000000..19bf6474
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-controller.ts
@@ -0,0 +1,218 @@
+import HandleExceptions from "../../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
+import { ServerResponse } from "../../../models/server-response";
+import ReportingProjectsBase from "./reporting-projects-base";
+import ReportingControllerBase from "../reporting-controller-base";
+import moment from "moment";
+import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../../shared/constants";
+import { getColor, int, formatDuration, formatLogText } from "../../../shared/utils";
+import db from "../../../config/db";
+
+export default class ReportingProjectsController extends ReportingProjectsBase {
+
+ private static flatString(text: string) {
+ return (text || "").split(",").map(s => `'${s}'`).join(",");
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, ["p.name"]);
+ const archived = req.query.archived === "true";
+
+ const teamId = this.getCurrentTeamId(req);
+
+ const statusesClause = req.query.statuses as string
+ ? `AND p.status_id IN (${this.flatString(req.query.statuses as string)})`
+ : "";
+
+ const healthsClause = req.query.healths as string
+ ? `AND p.health_id IN (${this.flatString(req.query.healths as string)})`
+ : "";
+
+ const categoriesClause = req.query.categories as string
+ ? `AND p.category_id IN (${this.flatString(req.query.categories as string)})`
+ : "";
+
+ // const projectManagersClause = req.query.project_managers as string
+ // ? `AND p.id IN (SELECT project_id from project_members WHERE team_member_id IN (${this.flatString(req.query.project_managers as string)}) AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))`
+ // : "";
+
+ const projectManagersClause = req.query.project_managers as string
+ ? `AND p.id IN(SELECT project_id FROM project_members WHERE team_member_id IN(SELECT id FROM team_members WHERE user_id IN (${this.flatString(req.query.project_managers as string)})) AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER'))`
+ : "";
+
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const teamFilterClause = `in_organization(p.team_id, $1)`;
+
+ const result = await ReportingControllerBase.getProjectsByTeam(teamId as string, size, offset, searchQuery, sortField, sortOrder, statusesClause, healthsClause, categoriesClause, archivedClause, teamFilterClause, projectManagersClause);
+
+ for (const project of result.projects) {
+ project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
+ project.days_left = ReportingControllerBase.getDaysLeft(project.end_date);
+ project.is_overdue = ReportingControllerBase.isOverdue(project.end_date);
+ if (project.days_left && project.is_overdue) {
+ project.days_left = project.days_left.toString().replace(/-/g, "");
+ }
+ project.is_today = this.isToday(project.end_date);
+ project.estimated_time = int(project.estimated_time);
+ project.actual_time = int(project.actual_time);
+ project.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
+ project.actual_time_string = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
+ project.tasks_stat = {
+ todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
+ doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
+ done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
+ };
+ if (project.update.length > 0) {
+ const [update] = project.update;
+ const placeHolders = update.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < update.mentions.length) {
+ update.content = update.content.replace(placeHolder, `
+ @${update.mentions[index].user_name} `);
+ }
+ });
+ }
+ project.comment = update.content;
+ }
+ if (project.last_activity) {
+ if (project.last_activity.attribute_type === "estimation") {
+ project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
+ project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
+ }
+ if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
+ project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
+ project.last_activity.log_text = await formatLogText(project.last_activity);
+ project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
+ project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ protected static getMinMaxDates(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `,(SELECT '${start}'::DATE )AS start_date, (SELECT '${end}'::DATE )AS end_date`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 day')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_WEEK)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 week')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_MONTH)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 month')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '3 months')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.ALL_TIME)
+ return ",(SELECT (MIN(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS start_date, (SELECT (MAX(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS end_date";
+
+ return "";
+ }
+
+ @HandleExceptions()
+ public static async getProjectTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const projectId = req.body.id;
+ const { duration, date_range } = req.body;
+
+ const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
+ const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range);
+
+ const q = `SELECT
+ (SELECT name FROM projects WHERE projects.id = $1) AS project_name,
+ (SELECT key FROM projects WHERE projects.id = $1) AS project_key,
+ (SELECT task_no FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_key_num,
+ (SELECT name FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_name,
+ task_work_log.time_spent,
+ (SELECT name FROM users WHERE users.id = task_work_log.user_id) AS user_name,
+ (SELECT email FROM users WHERE users.id = task_work_log.user_id) AS user_email,
+ (SELECT avatar_url FROM users WHERE users.id = task_work_log.user_id) AS avatar_url,
+ task_work_log.created_at
+ ${minMaxDateClause}
+ FROM task_work_log
+ WHERE
+ task_id IN (select id from tasks WHERE project_id = $1)
+ ${durationClause}
+ ORDER BY task_work_log.created_at DESC`;
+
+ const result = await db.query(q, [projectId]);
+
+ const formattedResult = await this.formatLog(result.rows);
+
+ const logGroups = await this.getTimeLogDays(formattedResult);
+
+ return res.status(200).send(new ServerResponse(true, logGroups));
+ }
+
+ private static async formatLog(result: any[]) {
+
+ result.forEach((row) => {
+ const duration = moment.duration(row.time_spent, "seconds");
+ row.time_spent_string = this.formatDuration(duration);
+ row.task_key = `${row.project_key}-${row.task_key_num}`;
+ });
+
+ return result;
+ }
+
+ private static async getTimeLogDays(result: any[]) {
+ if (result.length) {
+ const startDate = moment(result[0].start_date).isValid() ? moment(result[0].start_date, "YYYY-MM-DD").clone() : null;
+ const endDate = moment(result[0].end_date).isValid() ? moment(result[0].end_date, "YYYY-MM-DD").clone() : null;
+
+ const days = [];
+ const logDayGroups = [];
+
+ while (startDate && moment(startDate).isSameOrBefore(endDate)) {
+ days.push(startDate.clone().format("YYYY-MM-DD"));
+ startDate ? startDate.add(1, "day") : null;
+ }
+
+ for (const day of days) {
+ const logsForDay = result.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
+ if (logsForDay.length) {
+ logDayGroups.push({
+ log_day: day,
+ logs: logsForDay
+ });
+ }
+ }
+
+ return logDayGroups;
+
+ }
+ return [];
+ }
+
+ private static formatDuration(duration: moment.Duration) {
+ const empty = "0h 0m";
+ let format = "";
+
+ if (duration.asMilliseconds() === 0) return empty;
+
+ const h = ~~(duration.asHours());
+ const m = duration.minutes();
+ const s = duration.seconds();
+
+ if (h === 0 && s > 0) {
+ format = `${m}m ${s}s`;
+ } else if (h > 0 && s === 0) {
+ format = `${h}h ${m}m`;
+ } else if (h > 0 && s > 0) {
+ format = `${h}h ${m}m ${s}s`;
+ } else {
+ format = `${h}h ${m}m`;
+ }
+
+ return format;
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/projects/reporting-projects-export-controller.ts b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-export-controller.ts
new file mode 100644
index 00000000..9e68eb05
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/projects/reporting-projects-export-controller.ts
@@ -0,0 +1,279 @@
+import moment from "moment";
+import HandleExceptions from "../../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../../interfaces/worklenz-response";
+import ReportingProjectsBase from "./reporting-projects-base";
+import Excel from "exceljs";
+import ReportingControllerBase from "../reporting-controller-base";
+import { DATE_RANGES } from "../../../shared/constants";
+import db from "../../../config/db";
+
+export default class ReportingProjectsExportController extends ReportingProjectsBase {
+
+ @HandleExceptions()
+ public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamId = this.getCurrentTeamId(req);
+ const teamName = (req.query.team_name as string)?.trim() || null;
+
+ const results = await ReportingControllerBase.exportProjectsAll(teamId as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} projects - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Projects");
+
+ // define columns in table
+ sheet.columns = [
+ { header: "Project", key: "name", width: 30 },
+ { header: "Client", key: "client", width: 20 },
+ { header: "Category", key: "category", width: 20 },
+ { header: "Status", key: "status", width: 20 },
+ { header: "Start Date", key: "start_date", width: 20 },
+ { header: "End Date", key: "end_date", width: 20 },
+ { header: "Days Left/Overdue", key: "days_left", width: 20 },
+ { header: "Estimated Hours", key: "estimated_hours", width: 20 },
+ { header: "Actual Hours", key: "actual_hours", width: 20 },
+ { header: "Done Tasks(%)", key: "done_tasks", width: 20 },
+ { header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
+ { header: "Todo Tasks(%)", key: "todo_tasks", width: 20 },
+ { header: "Last Activity", key: "last_activity", width: 20 },
+ { header: "Project Health", key: "project_health", width: 20 },
+ { header: "Project Update", key: "project_update", width: 20 }
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Projects from ${teamName}`;
+ sheet.mergeCells("A1:O1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:O2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Project", "Client", "Category", "Status", "Start Date", "End Date", "Days Left/Overdue", "Estimated Hours", "Actual Hours", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)", "Last Activity", "Project Health", "Project Update"];
+ sheet.getRow(4).font = { bold: true };
+
+ // set table data
+ for (const item of results.projects) {
+
+ if (item.is_overdue && item.days_left) {
+ item.days_left = `-${item.days_left}`;
+ }
+
+ if (item.is_today) {
+ item.days_left = `Today`;
+ }
+
+ sheet.addRow({
+ name: item.name,
+ client: item.client ? item.client : "-",
+ category: item.category_name ? item.category_name : "-",
+ status: item.status_name ? item.status_name : "-",
+ start_date: item.start_date ? moment(item.start_date).format("YYYY-MM-DD") : "-",
+ end_date: item.end_date ? moment(item.end_date).format("YYYY-MM-DD") : "-",
+ days_left: item.days_left ? item.days_left.toString() : "-",
+ estimated_hours: item.estimated_time ? item.estimated_time.toString() : "-",
+ actual_hours: item.actual_time ? item.actual_time.toString() : "-",
+ done_tasks: item.tasks_stat.done ? `${item.tasks_stat.done}` : "-",
+ doing_tasks: item.tasks_stat.doing ? `${item.tasks_stat.doing}` : "-",
+ todo_tasks: item.tasks_stat.todo ? `${item.tasks_stat.todo}` : "-",
+ last_activity: item.last_activity ? item.last_activity.last_activity_string : "-",
+ project_health: item.project_health,
+ project_update: item.comment ? item.comment : "-",
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+ @HandleExceptions()
+ public static async exportProjectTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const result = await this.getProjectTimeLogs(req);
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${req.query.project_name} Time Logs - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Time Logs");
+
+ sheet.columns = [
+ { header: "Date", key: "date", width: 30 },
+ { header: "Log", key: "log", width: 120 },
+ ];
+
+ sheet.getCell("A1").value = `Time Logs from ${req.query.project_name}`;
+ sheet.mergeCells("A1:O1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:O2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ sheet.getRow(4).values = ["Date", "Log"];
+ sheet.getRow(4).font = { bold: true };
+
+ for (const row of result) {
+ for (const log of row.logs) {
+ sheet.addRow({
+ date: row.log_day,
+ log: `${log.user_name} logged ${log.time_spent_string} for ${log.task_name}`
+ });
+ }
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+ private static async getProjectTimeLogs(req: IWorkLenzRequest) {
+ const projectId = req.query.id;
+ const duration = req.query.duration as string;
+ const date_range = req.query.date_range as [];
+
+ const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
+ const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range);
+
+ const q = `SELECT
+ (SELECT name FROM projects WHERE projects.id = $1) AS project_name,
+ (SELECT key FROM projects WHERE projects.id = $1) AS project_key,
+ (SELECT task_no FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_key_num,
+ (SELECT name FROM tasks WHERE tasks.id = task_work_log.task_id) AS task_name,
+ task_work_log.time_spent,
+ (SELECT name FROM users WHERE users.id = task_work_log.user_id) AS user_name,
+ (SELECT email FROM users WHERE users.id = task_work_log.user_id) AS user_email,
+ (SELECT avatar_url FROM users WHERE users.id = task_work_log.user_id) AS avatar_url,
+ task_work_log.created_at
+ ${minMaxDateClause}
+ FROM task_work_log
+ WHERE
+ task_id IN (select id from tasks WHERE project_id = $1)
+ ${durationClause}
+ ORDER BY task_work_log.created_at DESC`;
+
+ const result = await db.query(q, [projectId]);
+
+ const formattedResult = await this.formatLog(result.rows);
+
+ const logGroups = await this.getTimeLogDays(formattedResult);
+
+ return logGroups;
+ }
+
+
+ protected static getMinMaxDates(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `,(SELECT '${start}'::DATE )AS start_date, (SELECT '${end}'::DATE )AS end_date`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 day')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_WEEK)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 week')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_MONTH)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '1 month')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return ",(SELECT (CURRENT_DATE - INTERVAL '3 months')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date";
+ if (key === DATE_RANGES.ALL_TIME)
+ return ",(SELECT (MIN(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS start_date, (SELECT (MAX(task_work_log.created_at)::DATE) FROM task_work_log WHERE task_id IN (SELECT id FROM tasks WHERE project_id = $1)) AS end_date";
+
+ return "";
+ }
+
+ private static async formatLog(result: any[]) {
+
+ result.forEach((row) => {
+ const duration = moment.duration(row.time_spent, "seconds");
+ row.time_spent_string = this.formatDuration(duration);
+ row.task_key = `${row.project_key}-${row.task_key_num}`;
+ });
+
+ return result;
+ }
+
+ private static async getTimeLogDays(result: any[]) {
+ if (result.length) {
+ const startDate = moment(result[0].start_date).isValid() ? moment(result[0].start_date, "YYYY-MM-DD").clone() : null;
+ const endDate = moment(result[0].end_date).isValid() ? moment(result[0].end_date, "YYYY-MM-DD").clone() : null;
+
+ const days = [];
+ const logDayGroups = [];
+
+ while (startDate && moment(startDate).isSameOrBefore(endDate)) {
+ days.push(startDate.clone().format("YYYY-MM-DD"));
+ startDate ? startDate.add(1, "day") : null;
+ }
+
+ for (const day of days) {
+ const logsForDay = result.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
+ if (logsForDay.length) {
+ logDayGroups.push({
+ log_day: day,
+ logs: logsForDay
+ });
+ }
+ }
+
+ return logDayGroups;
+
+ }
+ return [];
+ }
+
+ private static formatDuration(duration: moment.Duration) {
+ const empty = "0h 0m";
+ let format = "";
+
+ if (duration.asMilliseconds() === 0) return empty;
+
+ const h = ~~(duration.asHours());
+ const m = duration.minutes();
+ const s = duration.seconds();
+
+ if (h === 0 && s > 0) {
+ format = `${m}m ${s}s`;
+ } else if (h > 0 && s === 0) {
+ format = `${h}h ${m}m`;
+ } else if (h > 0 && s > 0) {
+ format = `${h}h ${m}m ${s}s`;
+ } else {
+ format = `${h}h ${m}m`;
+ }
+
+ return format;
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts
new file mode 100644
index 00000000..fb301d35
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/reporting-allocation-controller.ts
@@ -0,0 +1,502 @@
+import moment from "moment";
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { getColor, int, log_error } from "../../shared/utils";
+import ReportingControllerBase from "./reporting-controller-base";
+import { DATE_RANGES } from "../../shared/constants";
+import Excel from "exceljs";
+
+enum IToggleOptions {
+ 'WORKING_DAYS' = 'WORKING_DAYS', 'MAN_DAYS' = 'MAN_DAYS'
+}
+
+export default class ReportingAllocationController extends ReportingControllerBase {
+ private static async getTimeLoggedByProjects(projects: string[], users: string[], key: string, dateRange: string[], archived = false, user_id = ""): Promise {
+ try {
+ const projectIds = projects.map(p => `'${p}'`).join(",");
+ const userIds = users.map(u => `'${u}'`).join(",");
+
+ const duration = this.getDateRangeClause(key || DATE_RANGES.LAST_WEEK, dateRange);
+ const archivedClause = archived
+ ? ""
+ : `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND user_id = '${user_id}') `;
+
+ const projectTimeLogs = await this.getTotalTimeLogsByProject(archived, duration, projectIds, userIds, archivedClause);
+ const userTimeLogs = await this.getTotalTimeLogsByUser(archived, duration, projectIds, userIds);
+
+ const format = (seconds: number) => {
+ if (seconds === 0) return "-";
+ const duration = moment.duration(seconds, "seconds");
+ const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
+ return formattedDuration;
+ };
+
+ let totalProjectsTime = 0;
+ let totalUsersTime = 0;
+
+ for (const project of projectTimeLogs) {
+ if (project.all_tasks_count > 0) {
+ project.progress = Math.round((project.completed_tasks_count / project.all_tasks_count) * 100);
+ } else {
+ project.progress = 0;
+ }
+
+ let total = 0;
+ for (const log of project.time_logs) {
+ total += log.time_logged;
+ log.time_logged = format(log.time_logged);
+ }
+ project.totalProjectsTime = totalProjectsTime + total;
+ project.total = format(total);
+ }
+
+ for (const log of userTimeLogs) {
+ log.totalUsersTime = totalUsersTime + parseInt(log.time_logged)
+ log.time_logged = format(parseInt(log.time_logged));
+ }
+
+ return { projectTimeLogs, userTimeLogs };
+ } catch (error) {
+ log_error(error);
+ }
+ return [];
+ }
+
+ private static async getTotalTimeLogsByProject(archived: boolean, duration: string, projectIds: string, userIds: string, archivedClause = "") {
+ try {
+ const q = `SELECT projects.name,
+ projects.color_code,
+ sps.name AS status_name,
+ sps.color_code AS status_color_code,
+ sps.icon AS status_icon,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
+ AND project_id = projects.id) AS all_tasks_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE CASE WHEN ($1 IS TRUE) THEN project_id IS NOT NULL ELSE archived = FALSE END
+ AND project_id = projects.id
+ AND status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = projects.id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE))) AS completed_tasks_count,
+ (
+ SELECT COALESCE(JSON_AGG(r), '[]'::JSON)
+ FROM (
+ SELECT name,
+ (SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ LEFT JOIN tasks t ON task_work_log.task_id = t.id
+ WHERE user_id = users.id
+ AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
+ AND t.project_id = projects.id
+ ${duration}) AS time_logged
+ FROM users
+ WHERE id IN (${userIds})
+ ORDER BY name
+ ) r
+ ) AS time_logs
+ FROM projects
+ LEFT JOIN sys_project_statuses sps ON projects.status_id = sps.id
+ WHERE projects.id IN (${projectIds}) ${archivedClause};`;
+
+ const result = await db.query(q, [archived]);
+ return result.rows;
+ } catch (error) {
+ log_error(error);
+ return [];
+ }
+ }
+
+ private static async getTotalTimeLogsByUser(archived: boolean, duration: string, projectIds: string, userIds: string) {
+ try {
+ const q = `(SELECT id,
+ (SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ LEFT JOIN tasks t ON task_work_log.task_id = t.id
+ WHERE user_id = users.id
+ AND CASE WHEN ($1 IS TRUE) THEN t.project_id IS NOT NULL ELSE t.archived = FALSE END
+ AND t.project_id IN (${projectIds})
+ ${duration}) AS time_logged
+ FROM users
+ WHERE id IN (${userIds})
+ ORDER BY name);`;
+ const result = await db.query(q, [archived]);
+ return result.rows;
+ } catch (error) {
+ log_error(error);
+ return [];
+ }
+ }
+
+ private static async getUserIds(teamIds: any) {
+ try {
+ const q = `SELECT id, (SELECT name)
+ FROM users
+ WHERE id IN (SELECT user_id
+ FROM team_members
+ WHERE team_id IN (${teamIds}))
+ GROUP BY id
+ ORDER BY name`;
+ const result = await db.query(q, []);
+ return result.rows;
+ } catch (error) {
+ log_error(error);
+ return [];
+ }
+ }
+
+ @HandleExceptions()
+ public static async getAllocation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const teams = (req.body.teams || []) as string[]; // ids
+
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+ const projectIds = (req.body.projects || []) as string[];
+
+ if (!teamIds || !projectIds.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const users = await this.getUserIds(teamIds);
+ const userIds = users.map((u: any) => u.id);
+
+ const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, req.body.duration, req.body.date_range, (req.query.archived === "true"), req.user?.id);
+
+ for (const [i, user] of users.entries()) {
+ user.total_time = userTimeLogs[i].time_logged;
+ }
+
+ return res.status(200).send(new ServerResponse(true, { users, projects: projectTimeLogs }));
+ }
+
+ public static formatDurationDate = (date: Date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ };
+
+ @HandleExceptions()
+ public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teams = (req.query.teams as string)?.split(",");
+ const teamIds = teams.map(t => `'${t}'`).join(",");
+
+ const projectIds = (req.query.projects as string)?.split(",");
+
+ const duration = req.query.duration;
+
+ const dateRange = (req.query.date_range as string)?.split(",");
+
+ let start = "-";
+ let end = "-";
+
+ if (dateRange.length === 2) {
+ start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
+ end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
+ } else {
+ switch (duration) {
+ case DATE_RANGES.YESTERDAY:
+ start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_WEEK:
+ start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_MONTH:
+ start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_QUARTER:
+ start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
+ break;
+ }
+ end = moment().format("YYYY-MM-DD").toString();
+ }
+
+ const users = await this.getUserIds(teamIds);
+ const userIds = users.map((u: any) => u.id);
+
+ const { projectTimeLogs, userTimeLogs } = await this.getTimeLoggedByProjects(projectIds, userIds, duration as string, dateRange, (req.query.include_archived === "true"), req.user?.id);
+
+ for (const [i, user] of users.entries()) {
+ user.total_time = userTimeLogs[i].time_logged;
+ }
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `Reporting Time Sheet - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Reporting Time Sheet");
+
+ sheet.columns = [
+ { header: "Project", key: "project", width: 25 },
+ { header: "Logged Time", key: "logged_time", width: 20 },
+ { header: "Total", key: "total", width: 25 },
+ ];
+
+ sheet.getCell("A1").value = `Reporting Time Sheet`;
+ sheet.mergeCells("A1:G1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:G2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ sheet.mergeCells("A3:D3");
+
+ let totalProjectTime = 0;
+ let totalMemberTime = 0;
+
+ if (projectTimeLogs.length > 0) {
+ const rowTop = sheet.getRow(5);
+ rowTop.getCell(1).value = "";
+
+ users.forEach((user: { id: string, name: string, total_time: string }, index: any) => {
+ rowTop.getCell(index + 2).value = user.name;
+ });
+
+ rowTop.getCell(users.length + 2).value = "Total";
+
+ rowTop.font = {
+ bold: true
+ };
+
+ for (const project of projectTimeLogs) {
+
+ const rowValues = [];
+ rowValues[1] = project.name;
+ project.time_logs.forEach((log: any, index: any) => {
+ rowValues[index + 2] = log.time_logged === "0h 0m 0s" ? "-" : log.time_logged;
+ });
+ rowValues[project.time_logs.length + 2] = project.total;
+ sheet.addRow(rowValues);
+
+ const { lastRow } = sheet;
+ if (lastRow) {
+ const totalCell = lastRow.getCell(project.time_logs.length + 2);
+ totalCell.style.font = { bold: true };
+ }
+ totalProjectTime = totalProjectTime + project.totalProjectsTime
+ }
+
+ const rowBottom = sheet.getRow(projectTimeLogs.length + 6);
+ rowBottom.getCell(1).value = "Total";
+ rowBottom.getCell(1).style.font = { bold: true };
+ userTimeLogs.forEach((log: { id: string, time_logged: string, totalUsersTime: number }, index: any) => {
+ totalMemberTime = totalMemberTime + log.totalUsersTime
+ rowBottom.getCell(index + 2).value = log.time_logged;
+ });
+ rowBottom.font = {
+ bold: true
+ };
+
+ }
+
+ const format = (seconds: number) => {
+ if (seconds === 0) return "-";
+ const duration = moment.duration(seconds, "seconds");
+ const formattedDuration = `${~~(duration.asHours())}h ${duration.minutes()}m ${duration.seconds()}s`;
+ return formattedDuration;
+ };
+
+ const projectTotalTimeRow = sheet.getRow(projectTimeLogs.length + 8);
+ projectTotalTimeRow.getCell(1).value = "Total logged time of Projects"
+ projectTotalTimeRow.getCell(2).value = `${format(totalProjectTime)}`
+ projectTotalTimeRow.getCell(1).style.font = { bold: true };
+ projectTotalTimeRow.getCell(2).style.font = { bold: true };
+
+ const membersTotalTimeRow = sheet.getRow(projectTimeLogs.length + 9);
+ membersTotalTimeRow.getCell(1).value = "Total logged time of Members"
+ membersTotalTimeRow.getCell(2).value = `${format(totalMemberTime)}`
+ membersTotalTimeRow.getCell(1).style.font = { bold: true };
+ membersTotalTimeRow.getCell(2).style.font = { bold: true };
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+
+ @HandleExceptions()
+ public static async getProjectTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const archived = req.query.archived === "true";
+
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const projects = (req.body.projects || []) as string[];
+ const projectIds = projects.map(p => `'${p}'`).join(",");
+
+ if (!teamIds || !projectIds.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const { duration, date_range } = req.body;
+
+ const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
+
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const q = `
+ SELECT p.id,
+ p.name,
+ (SELECT SUM(time_spent)) AS logged_time,
+ SUM(total_minutes) AS estimated,
+ color_code
+ FROM projects p
+ LEFT JOIN tasks t ON t.project_id = p.id
+ LEFT JOIN task_work_log ON task_work_log.task_id = t.id
+ WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
+ GROUP BY p.id, p.name
+ ORDER BY logged_time DESC;`;
+ const result = await db.query(q, []);
+
+ const data = [];
+
+ for (const project of result.rows) {
+ project.value = project.logged_time ? parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2)) : 0;
+ project.estimated_value = project.estimated ? parseFloat(moment.duration(project.estimated, "minutes").asHours().toFixed(2)) : 0;
+
+ if (project.value > 0 ) {
+ data.push(project);
+ }
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+
+ @HandleExceptions()
+ public static async getMemberTimeSheets(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const archived = req.query.archived === "true";
+
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const projects = (req.body.projects || []) as string[];
+ const projectIds = projects.map(p => `'${p}'`).join(",");
+
+ if (!teamIds || !projectIds.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const { duration, date_range } = req.body;
+
+ const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const q = `
+ SELECT tmiv.email, tmiv.name, SUM(time_spent) AS logged_time
+ FROM team_member_info_view tmiv
+ LEFT JOIN task_work_log ON task_work_log.user_id = tmiv.user_id
+ LEFT JOIN tasks t ON t.id = task_work_log.task_id
+ LEFT JOIN projects p ON p.id = t.project_id AND p.team_id = tmiv.team_id
+ WHERE p.id IN (${projectIds})
+ ${durationClause} ${archivedClause}
+ GROUP BY tmiv.email, tmiv.name
+ ORDER BY logged_time DESC;`;
+ const result = await db.query(q, []);
+
+ for (const member of result.rows) {
+ member.value = member.logged_time ? parseFloat(moment.duration(member.logged_time, "seconds").asHours().toFixed(2)) : 0;
+ member.color_code = getColor(member.name);
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ private static getEstimated(project: any, type: string) {
+
+ switch (type) {
+ case IToggleOptions.MAN_DAYS:
+ return project.estimated_man_days ?? 0;;
+
+ case IToggleOptions.WORKING_DAYS:
+ return project.estimated_working_days ?? 0;;
+
+ default:
+ return 0;
+ }
+ }
+
+ @HandleExceptions()
+ public static async getEstimatedVsActual(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const archived = req.query.archived === "true";
+
+ const teams = (req.body.teams || []) as string[]; // ids
+ const teamIds = teams.map(id => `'${id}'`).join(",");
+
+ const projects = (req.body.projects || []) as string[];
+ const projectIds = projects.map(p => `'${p}'`).join(",");
+ const { type } = req.body;
+
+ if (!teamIds || !projectIds.length)
+ return res.status(200).send(new ServerResponse(true, { users: [], projects: [] }));
+
+ const { duration, date_range } = req.body;
+
+ const durationClause = this.getDateRangeClause(duration || DATE_RANGES.LAST_WEEK, date_range);
+
+ const archivedClause = archived
+ ? ""
+ : `AND p.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = p.id AND user_id = '${req.user?.id}') `;
+
+ const q = `
+ SELECT p.id,
+ p.name,
+ p.end_date,
+ p.hours_per_day::INT,
+ p.estimated_man_days::INT,
+ p.estimated_working_days::INT,
+ (SELECT SUM(time_spent)) AS logged_time,
+ (SELECT COALESCE(SUM(total_minutes), 0)
+ FROM tasks
+ WHERE project_id = p.id) AS estimated,
+ color_code
+ FROM projects p
+ LEFT JOIN tasks t ON t.project_id = p.id
+ LEFT JOIN task_work_log ON task_work_log.task_id = t.id
+ WHERE p.id IN (${projectIds}) ${durationClause} ${archivedClause}
+ GROUP BY p.id, p.name
+ ORDER BY logged_time DESC;`;
+ const result = await db.query(q, []);
+
+ const data = [];
+
+ for (const project of result.rows) {
+ const durationInHours = parseFloat(moment.duration(project.logged_time, "seconds").asHours().toFixed(2));
+ const hoursPerDay = parseInt(project.hours_per_day ?? 1);
+
+ project.value = parseFloat((durationInHours / hoursPerDay).toFixed(2)) ?? 0;
+
+ project.estimated_value = this.getEstimated(project, type);
+ project.estimated_man_days = project.estimated_man_days ?? 0;
+ project.estimated_working_days = project.estimated_working_days ?? 0;
+ project.hours_per_day = project.hours_per_day ?? 0;
+
+ if (project.value > 0 || project.estimated_value > 0 ) {
+ data.push(project);
+ }
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+}
diff --git a/worklenz-backend/src/controllers/reporting/reporting-controller-base.ts b/worklenz-backend/src/controllers/reporting/reporting-controller-base.ts
new file mode 100644
index 00000000..a1adaccb
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/reporting-controller-base.ts
@@ -0,0 +1,604 @@
+import WorklenzControllerBase from "../worklenz-controller-base";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import db from "../../config/db";
+import moment from "moment";
+import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
+import { formatDuration, formatLogText, getColor, int } from "../../shared/utils";
+
+export default abstract class ReportingControllerBase extends WorklenzControllerBase {
+ protected static getPercentage(n: number, total: number) {
+ return +(n ? (n / total) * 100 : 0).toFixed();
+ }
+
+ protected static getCurrentTeamId(req: IWorkLenzRequest): string | null {
+ return req.user?.team_id ?? null;
+ }
+
+ protected static async getTotalTasksCount(projectId: string | null) {
+ const q = `
+ SELECT COUNT(*) AS count
+ FROM tasks
+ WHERE project_id = $1;
+ `;
+ const result = await db.query(q, [projectId]);
+ const [data] = result.rows;
+ return data.count || 0;
+ }
+
+ protected static async getArchivedProjectsClause(archived = false, user_id: string, column_name: string) {
+ return archived
+ ? ""
+ : `AND ${column_name} NOT IN (SELECT project_id FROM archived_projects WHERE project_id = ${column_name} AND user_id = '${user_id}') `;
+ }
+
+ protected static async getAllTasks(projectId: string | null) {
+ const q = `
+ SELECT id,
+ name,
+ parent_task_id,
+ parent_task_id IS NOT NULL AS is_sub_task,
+ status_id AS status,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status_name,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = status_id)) AS status_color,
+ priority_id AS priority,
+ (SELECT value FROM task_priorities WHERE id = tasks.priority_id) AS priority_value,
+ (SELECT name FROM task_priorities WHERE id = tasks.priority_id) AS priority_name,
+ (SELECT color_code FROM task_priorities WHERE id = tasks.priority_id) AS priority_color,
+ end_date,
+ (SELECT phase_id FROM task_phase WHERE task_id = tasks.id) AS phase_id,
+ (SELECT name
+ FROM project_phases
+ WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = tasks.id)) AS phase_name,
+ completed_at,
+ total_minutes,
+ (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = tasks.id) AS total_seconds_spent
+ FROM tasks
+ WHERE project_id = $1
+ ORDER BY name;
+ `;
+ const result = await db.query(q, [projectId]);
+
+ for (const item of result.rows) {
+ const endDate = moment(item.end_date);
+ const completedDate = moment(item.completed_at);
+ const overdueDays = completedDate.diff(endDate, "days");
+
+ if (overdueDays > 0) {
+ item.overdue_days = overdueDays.toString();
+ } else {
+ item.overdue_days = "0";
+ }
+
+ item.total_minutes_spent = Math.ceil(item.total_seconds_spent / 60);
+
+ if (~~(item.total_minutes_spent) > ~~(item.total_minutes)) {
+ const overlogged_time = ~~(item.total_minutes_spent) - ~~(item.total_minutes);
+ item.overlogged_time_string = formatDuration(moment.duration(overlogged_time, "minutes"));
+ } else {
+ item.overlogged_time_string = `0h 0m`;
+ }
+ }
+
+ return result.rows;
+ }
+
+ protected static getDateRangeClause(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ let query = `AND task_work_log.created_at::DATE >= '${start}'::DATE AND task_work_log.created_at < '${end}'::DATE + INTERVAL '1 day'`;
+
+ if (start === end) {
+ query = `AND task_work_log.created_at::DATE = '${start}'::DATE`;
+ }
+
+ return query;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE";
+ if (key === DATE_RANGES.LAST_WEEK)
+ return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
+ if (key === DATE_RANGES.LAST_MONTH)
+ return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return "AND task_work_log.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND task_work_log.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'";
+
+ return "";
+ }
+
+ protected static formatEndDate(endDate: string) {
+ const end = moment(endDate).format("YYYY-MM-DD");
+ const fEndDate = moment(end);
+ return fEndDate;
+ }
+
+ protected static formatCurrentDate() {
+ const current = moment().format("YYYY-MM-DD");
+ const fCurrentDate = moment(current);
+ return fCurrentDate;
+ }
+
+ protected static getDaysLeft(endDate: string): number | null {
+ if (!endDate) return null;
+
+ const fCurrentDate = this.formatCurrentDate();
+ const fEndDate = this.formatEndDate(endDate);
+
+ return fEndDate.diff(fCurrentDate, "days");
+ }
+
+ protected static isOverdue(endDate: string): boolean {
+ if (!endDate) return false;
+
+ const fCurrentDate = this.formatCurrentDate();
+ const fEndDate = this.formatEndDate(endDate);
+
+ return fEndDate.isBefore(fCurrentDate);
+ }
+
+ protected static isToday(endDate: string): boolean {
+ if (!endDate) return false;
+
+ const fCurrentDate = this.formatCurrentDate();
+ const fEndDate = this.formatEndDate(endDate);
+
+ return fEndDate.isSame(fCurrentDate);
+ }
+
+
+ public static async getProjectsByTeam(
+ teamId: string,
+ size: string | number | null,
+ offset: string | number | null,
+ searchQuery: string | null,
+ sortField: string,
+ sortOrder: string,
+ statusClause: string,
+ healthClause: string,
+ categoryClause: string,
+ archivedClause = "",
+ teamFilterClause: string,
+ projectManagersClause: string) {
+
+ const q = `SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT p.id,
+ p.name,
+ p.color_code,
+
+ p.health_id AS project_health,
+ (SELECT color_code
+ FROM sys_project_healths
+ WHERE sys_project_healths.id = p.health_id) AS health_color,
+
+ pc.id AS category_id,
+ pc.name AS category_name,
+ pc.color_code AS category_color,
+
+ (SELECT name FROM clients WHERE id = p.client_id) AS client,
+
+ p.team_id,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+
+ ps.id AS status_id,
+ ps.name AS status_name,
+ ps.color_code AS status_color,
+ ps.icon AS status_icon,
+
+ start_date,
+ end_date,
+
+ (SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
+ FROM (SELECT team_member_id AS id,
+ (SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
+ FROM (SELECT name,
+ email,
+ avatar_url
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_member_id = pm.team_member_id
+ AND tmiv.team_id = (SELECT team_id FROM projects WHERE id = p.id)) pmi) AS project_manager_info,
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = pm.team_member_id
+ AND email_invitations.team_id = (SELECT team_id
+ FROM team_member_info_view
+ WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
+ (SELECT active FROM team_members WHERE id = pm.team_member_id)
+ FROM project_members pm
+ WHERE project_id =p.id
+ AND project_access_level_id = (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager,
+
+ (SELECT COALESCE(SUM(total_minutes), 0)
+ FROM tasks
+ WHERE project_id = p.id) AS estimated_time,
+
+ (SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ WHERE task_id = tasks.id))
+ FROM tasks
+ WHERE project_id = p.id) AS actual_time,
+
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT COUNT(ta.id) AS total,
+ COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
+ COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
+ COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
+ FROM tasks ta
+ WHERE project_id = p.id) rec) AS tasks_stat,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pu.content AS content,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT u.name AS user_name,
+ u.email AS user_email
+ FROM project_comment_mentions pcm
+ LEFT JOIN users u ON pcm.informed_by = u.id
+ WHERE pcm.comment_id = pu.id) rec) AS mentions,
+ pu.updated_at
+ FROM project_comments pu
+ WHERE pu.project_id = p.id
+ ORDER BY pu.updated_at DESC
+ LIMIT 1) AS rec) AS update,
+
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT attribute_type,
+ log_type,
+ -- new case,
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
+ ELSE (old_value) END) AS previous,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT name FROM users WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'label')
+ THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
+ ELSE (new_value) END) AS current,
+ (SELECT name
+ FROM users
+ WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
+ (SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (CASE
+ WHEN (new_value IS NOT NULL)
+ THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
+ ELSE (next_string) END) AS name,
+ (SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS assigned_user,
+ (SELECT name FROM tasks WHERE tasks.id = tal.task_id)
+ FROM task_activity_logs tal
+ WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
+ ORDER BY tal.created_at DESC
+ LIMIT 1) rec) AS last_activity
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause}
+ ORDER BY ${sortField} ${sortOrder}
+ LIMIT $2 OFFSET $3) t) AS projects
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE ${teamFilterClause} ${searchQuery} ${healthClause} ${statusClause} ${categoryClause} ${projectManagersClause} ${archivedClause};`;
+ const result = await db.query(q, [teamId, size, offset]);
+ const [data] = result.rows;
+
+ for (const project of data.projects) {
+ if (project.project_manager) {
+ project.project_manager.name = project.project_manager.project_manager_info.name;
+ project.project_manager.avatar_url = project.project_manager.project_manager_info.avatar_url;
+ project.project_manager.color_code = getColor(project.project_manager.name);
+ }
+ }
+
+ return data;
+ }
+
+ public static convertMinutesToHoursAndMinutes(totalMinutes: number) {
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+ return `${hours}h ${minutes}m`;
+ }
+
+ public static convertSecondsToHoursAndMinutes(seconds: number) {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ return `${hours}h ${minutes}m`;
+ }
+
+ public static async exportProjects(teamId: string) {
+ const q = `SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT p.id,
+ p.name,
+ (SELECT name
+ FROM sys_project_healths
+ WHERE sys_project_healths.id = p.health_id) AS project_health,
+ pc.name AS category_name,
+ (SELECT name FROM clients WHERE id = p.client_id) AS client,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+ ps.name AS status_name,
+ start_date,
+ end_date,
+ (SELECT COALESCE(SUM(total_minutes), 0)
+ FROM tasks
+ WHERE project_id = p.id) AS estimated_time,
+ (SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ WHERE task_id = tasks.id))
+ FROM tasks
+ WHERE project_id = p.id) AS actual_time,
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT COUNT(ta.id) AS total,
+ COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
+ COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
+ COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
+ FROM tasks ta
+ WHERE project_id = p.id) rec) AS tasks_stat,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pu.content AS content,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT u.name AS user_name,
+ u.email AS user_email
+ FROM project_comment_mentions pcm
+ LEFT JOIN users u ON pcm.informed_by = u.id
+ WHERE pcm.comment_id = pu.id) rec) AS mentions,
+ pu.updated_at
+ FROM project_comments pu
+ WHERE pu.project_id = p.id
+ ORDER BY pu.updated_at DESC
+ LIMIT 1) AS rec) AS update,
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT attribute_type,
+ log_type,
+ -- new case,
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
+ ELSE (old_value) END) AS previous,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT name FROM users WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'label')
+ THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
+ ELSE (new_value) END) AS current,
+ (SELECT name
+ FROM users
+ WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
+ (SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (CASE
+ WHEN (new_value IS NOT NULL)
+ THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
+ ELSE (next_string) END) AS name,
+ (SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS assigned_user,
+ (SELECT name FROM tasks WHERE tasks.id = tal.task_id)
+ FROM task_activity_logs tal
+ WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
+ ORDER BY tal.created_at
+ LIMIT 1) rec) AS last_activity
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE p.team_id = $1 ORDER BY p.name) t) AS projects
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE p.team_id = $1;`;
+
+ const result = await db.query(q, [teamId]);
+
+ const [data] = result.rows;
+
+ for (const project of data.projects) {
+ project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
+ project.days_left = this.getDaysLeft(project.end_date);
+ project.is_overdue = this.isOverdue(project.end_date);
+ if (project.days_left && project.is_overdue) {
+ project.days_left = project.days_left.toString().replace(/-/g, "");
+ }
+ project.is_today = this.isToday(project.end_date);
+ project.estimated_time = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
+ project.actual_time = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
+ project.tasks_stat = {
+ todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
+ doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
+ done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
+ };
+ if (project.update.length > 0) {
+ const update = project.update[0];
+ const placeHolders = update.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < update.mentions.length) {
+ update.content = update.content.replace(placeHolder, ` @${update.mentions[index].user_name} `);
+ }
+ });
+ }
+ project.comment = update.content;
+ }
+ if (project.last_activity) {
+ if (project.last_activity.attribute_type === "estimation") {
+ project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
+ project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
+ }
+ if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
+ project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
+ project.last_activity.log_text = await formatLogText(project.last_activity);
+ project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
+ project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
+ }
+ }
+ return data;
+ }
+
+ public static async exportProjectsAll(teamId: string) {
+ const q = `SELECT COUNT(*) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT p.id,
+ p.name,
+ (SELECT name
+ FROM sys_project_healths
+ WHERE sys_project_healths.id = p.health_id) AS project_health,
+ pc.name AS category_name,
+ (SELECT name FROM clients WHERE id = p.client_id) AS client,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team_name,
+ ps.name AS status_name,
+ start_date,
+ end_date,
+ (SELECT COALESCE(SUM(total_minutes), 0)
+ FROM tasks
+ WHERE project_id = p.id) AS estimated_time,
+ (SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ WHERE task_id = tasks.id))
+ FROM tasks
+ WHERE project_id = p.id) AS actual_time,
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT COUNT(ta.id) AS total,
+ COUNT(CASE WHEN is_completed(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS done,
+ COUNT(CASE WHEN is_doing(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS doing,
+ COUNT(CASE WHEN is_todo(ta.status_id, ta.project_id) IS TRUE THEN 1 END) AS todo
+ FROM tasks ta
+ WHERE project_id = p.id) rec) AS tasks_stat,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pu.content AS content,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT u.name AS user_name,
+ u.email AS user_email
+ FROM project_comment_mentions pcm
+ LEFT JOIN users u ON pcm.informed_by = u.id
+ WHERE pcm.comment_id = pu.id) rec) AS mentions,
+ pu.updated_at
+ FROM project_comments pu
+ WHERE pu.project_id = p.id
+ ORDER BY pu.updated_at DESC
+ LIMIT 1) AS rec) AS update,
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT attribute_type,
+ log_type,
+ -- new case,
+ (CASE
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
+ ELSE (old_value) END) AS previous,
+
+ -- new case
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT name FROM users WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'label')
+ THEN (SELECT name FROM team_labels WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'status')
+ THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'priority')
+ THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
+ ELSE (new_value) END) AS current,
+ (SELECT name
+ FROM users
+ WHERE id = (SELECT reporter_id FROM tasks WHERE id = tal.task_id)),
+ (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM users WHERE users.id = tal.user_id),
+ (SELECT avatar_url FROM users WHERE users.id = tal.user_id)) rec) AS done_by,
+ (CASE
+ WHEN (attribute_type = 'assignee')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (CASE
+ WHEN (new_value IS NOT NULL)
+ THEN (SELECT name FROM users WHERE users.id = new_value::UUID)
+ ELSE (next_string) END) AS name,
+ (SELECT avatar_url FROM users WHERE users.id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS assigned_user,
+ (SELECT name FROM tasks WHERE tasks.id = tal.task_id)
+ FROM task_activity_logs tal
+ WHERE task_id IN (SELECT id FROM tasks t WHERE t.project_id = p.id)
+ ORDER BY tal.created_at
+ LIMIT 1) rec) AS last_activity
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE in_organization(p.team_id, $1) ORDER BY p.name) t) AS projects
+ FROM projects p
+ LEFT JOIN project_categories pc ON pc.id = p.category_id
+ LEFT JOIN sys_project_statuses ps ON p.status_id = ps.id
+ WHERE in_organization(p.team_id, $1);`;
+
+ const result = await db.query(q, [teamId]);
+
+ const [data] = result.rows;
+
+ for (const project of data.projects) {
+ project.team_color = getColor(project.team_name) + TASK_PRIORITY_COLOR_ALPHA;
+ project.days_left = this.getDaysLeft(project.end_date);
+ project.is_overdue = this.isOverdue(project.end_date);
+ if (project.days_left && project.is_overdue) {
+ project.days_left = project.days_left.toString().replace(/-/g, "");
+ }
+ project.is_today = this.isToday(project.end_date);
+ project.estimated_time = this.convertMinutesToHoursAndMinutes(int(project.estimated_time));
+ project.actual_time = this.convertSecondsToHoursAndMinutes(int(project.actual_time));
+ project.tasks_stat = {
+ todo: this.getPercentage(int(project.tasks_stat.todo), +project.tasks_stat.total),
+ doing: this.getPercentage(int(project.tasks_stat.doing), +project.tasks_stat.total),
+ done: this.getPercentage(int(project.tasks_stat.done), +project.tasks_stat.total)
+ };
+ if (project.update.length > 0) {
+ const update = project.update[0];
+ const placeHolders = update.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < update.mentions.length) {
+ update.content = update.content.replace(placeHolder, ` @${update.mentions[index].user_name} `);
+ }
+ });
+ }
+ project.comment = update.content;
+ }
+ if (project.last_activity) {
+ if (project.last_activity.attribute_type === "estimation") {
+ project.last_activity.previous = formatDuration(moment.duration(project.last_activity.previous, "minutes"));
+ project.last_activity.current = formatDuration(moment.duration(project.last_activity.current, "minutes"));
+ }
+ if (project.last_activity.assigned_user) project.last_activity.assigned_user.color_code = getColor(project.last_activity.assigned_user.name);
+ project.last_activity.done_by.color_code = getColor(project.last_activity.done_by.name);
+ project.last_activity.log_text = await formatLogText(project.last_activity);
+ project.last_activity.attribute_type = project.last_activity.attribute_type?.replace(/_/g, " ");
+ project.last_activity.last_activity_string = `${project.last_activity.done_by.name} ${project.last_activity.log_text} ${project.last_activity.attribute_type}`;
+ }
+ }
+ return data;
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/reporting/reporting-info-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-info-controller.ts
new file mode 100644
index 00000000..81b088df
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/reporting-info-controller.ts
@@ -0,0 +1,20 @@
+import ReportingControllerBase from "./reporting-controller-base";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../../interfaces/worklenz-response";
+import db from "../../config/db";
+import {ServerResponse} from "../../models/server-response";
+
+export default class ReportingInfoController extends ReportingControllerBase {
+ @HandleExceptions()
+ public static async getInfo(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT organization_name
+ FROM organizations
+ WHERE user_id = (SELECT user_id FROM teams WHERE id = $1);
+ `;
+ const result = await db.query(q, [this.getCurrentTeamId(req)]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+}
diff --git a/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts
new file mode 100644
index 00000000..a4c6cd12
--- /dev/null
+++ b/worklenz-backend/src/controllers/reporting/reporting-members-controller.ts
@@ -0,0 +1,1301 @@
+import moment from "moment";
+import db from "../../config/db";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { DATE_RANGES, TASK_PRIORITY_COLOR_ALPHA } from "../../shared/constants";
+import { formatDuration, getColor, int } from "../../shared/utils";
+import ReportingControllerBase from "./reporting-controller-base";
+import Excel from "exceljs";
+
+export default class ReportingMembersController extends ReportingControllerBase {
+
+ private static async getMembers(
+ teamId: string, searchQuery = "",
+ size: number | null = null,
+ offset: number | null = null,
+ teamsClause = "",
+ key = DATE_RANGES.LAST_WEEK,
+ dateRange: string[] = [],
+ includeArchived: boolean,
+ userId: string
+ ) {
+ const pagingClause = (size !== null && offset !== null) ? `LIMIT ${size} OFFSET ${offset}` : "";
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${userId}')`;
+
+ // const durationFilterClause = this.memberTasksDurationFilter(key, dateRange);
+ const assignClause = this.memberAssignDurationFilter(key, dateRange);
+ const completedDurationClasue = this.completedDurationFilter(key, dateRange);
+ const overdueActivityLogsClause = this.getActivityLogsOverdue(key, dateRange);
+ const activityLogCreationFilter = this.getActivityLogsCreationClause(key, dateRange);
+
+ const q = `SELECT COUNT(DISTINCT email) AS total,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
+ FROM (SELECT team_member_id AS id,
+ name,
+ avatar_url,
+ email,
+ (SELECT COUNT(project_id)
+ FROM project_members pm
+ WHERE pm.team_member_id = tmiv.team_member_id) AS projects,
+
+ (SELECT GREATEST(
+ (SELECT MAX(created_at) FROM task_activity_logs WHERE user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id) AND team_id = $1),
+ (SELECT MAX(created_at) FROM task_work_log WHERE user_id = (SELECT user_id FROM team_members WHERE id = tmiv.team_member_id)
+ AND task_id IN (SELECT id FROM tasks t WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1 )
+ )))) AS last_user_activity,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id ${archivedClause}) AS total_tasks,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id ${assignClause} ${archivedClause}) AS tasks,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_completed(status_id, t.project_id) ${archivedClause}) AS total_completed,
+
+ (SELECT COUNT(*) FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id AND is_completed(status_id, t.project_id) ${completedDurationClasue} ${archivedClause}) AS completed,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_doing(status_id, t.project_id) ${archivedClause}) AS total_ongoing,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_doing(status_id, t.project_id) ${archivedClause}) AS ongoing,
+
+ (SELECT COUNT(*) FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id ${overdueActivityLogsClause} ${archivedClause}) AS overdue,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_todo(status_id, t.project_id) ${archivedClause}) AS todo,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_todo((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS todo_by_activity_logs,
+
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE team_member_id = tmiv.team_member_id
+ AND is_doing((SELECT new_value FROM task_activity_logs tl WHERE tl.task_id = t.id AND tl.attribute_type = 'status' ${activityLogCreationFilter} ORDER BY tl.created_at DESC LIMIT 1)::UUID, t.project_id) ${archivedClause}) AS ongoing_by_activity_logs
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_id = $1 ${teamsClause}
+ AND tmiv.team_member_id IN (SELECT team_member_id
+ FROM project_members
+ WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
+ ${searchQuery}
+ GROUP BY email, name, avatar_url, team_member_id, tmiv.team_id
+ ORDER BY last_user_activity DESC NULLS LAST
+
+ ${pagingClause}) t) AS members
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_id = $1 ${teamsClause}
+ AND tmiv.team_member_id IN (SELECT team_member_id
+ FROM project_members
+ WHERE project_id IN (SELECT id FROM projects WHERE projects.team_id = tmiv.team_id))
+ ${searchQuery}`;
+ const result = await db.query(q, [teamId]);
+ const [data] = result.rows;
+
+ for (const member of data.members) {
+ member.color_code = getColor(member.name) + TASK_PRIORITY_COLOR_ALPHA;
+ member.tasks_stat = {
+ todo: this.getPercentage(int(member.todo_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)),
+ doing: this.getPercentage(int(member.ongoing_by_activity_logs), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs)),
+ done: this.getPercentage(int(member.completed), + (member.completed + member.todo_by_activity_logs + member.ongoing_by_activity_logs))
+ };
+ member.member_teams = this.createTagList(member.member_teams, 2);
+ }
+ return data;
+ }
+
+ private static flatString(text: string) {
+ return (text || "").split(" ").map(s => `'${s}'`).join(",");
+ }
+
+ protected static memberTasksDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND t.end_date::DATE = '${start}'::DATE`;
+ }
+
+ return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static memberAssignDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND ta.updated_at::DATE = '${start}'::DATE`;
+ }
+
+ return `AND ta.updated_at::DATE >= '${start}'::DATE AND ta.updated_at::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND ta.updated_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ta.updated_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static completedDurationFilter(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND t.completed_at::DATE = '${start}'::DATE`;
+ }
+
+ return `AND t.completed_at::DATE >= '${start}'::DATE AND t.completed_at::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.completed_at::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.completed_at::DATE < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ protected static getOverdueClause(key: string, dateRange: string[]) {
+
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND t.end_date::DATE = '${start}'::DATE`;
+ }
+
+ return `AND t.end_date::DATE >= '${start}'::DATE AND t.end_date::DATE <= '${end}'::DATE`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND t.end_date::DATE < NOW()::DATE`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND t.end_date::DATE >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND t.end_date::DATE < NOW()::DATE`;
+
+
+ return ` AND t.end_date::DATE < NOW()::DATE `;
+ }
+
+ protected static getTaskSelectorClause() {
+ return `SELECT t.id,
+ t.name,
+ t.project_id,
+ (SELECT name FROM projects WHERE id = t.project_id) AS project_name,
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+
+ t.end_date,
+ t.completed_at,
+
+ (CASE
+ WHEN (CURRENT_DATE::DATE > end_date::DATE AND
+ status_id IN (SELECT id
+ FROM task_statuses
+ WHERE project_id = t.project_id
+ AND category_id IN
+ (SELECT id FROM sys_task_status_categories WHERE is_done IS FALSE)))
+ THEN CURRENT_DATE::DATE - end_date::DATE
+ ELSE 0 END) AS days_overdue,
+
+ (SELECT name FROM task_statuses WHERE id = t.status_id) AS status_name,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id AND t.project_id = t.project_id)) AS status_color,
+
+ (SELECT name FROM task_priorities WHERE id = t.priority_id) AS priority_name,
+ (SELECT color_code FROM task_priorities WHERE id = t.priority_id) AS priority_color,
+
+ (SELECT name FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
+ (SELECT color_code FROM project_phases WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color,
+
+ (total_minutes * 60) AS total_minutes,
+ (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) AS time_logged,
+ ((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id AND ta.team_member_id = $1) - (total_minutes * 60)) AS overlogged_time`;
+ }
+
+ protected static getActivityLogsOverdue(key: string, dateRange: string[]) {
+
+ if (dateRange.length === 2) {
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND is_overdue_for_date(t.id, '${end}'::DATE)`;
+ }
+
+ return `AND is_overdue_for_date(t.id, NOW()::DATE)`;
+ }
+
+ protected static getActivityLogsCreationClause(key: string, dateRange: string[]) {
+ if (dateRange.length === 2) {
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `AND tl.created_at::DATE <= '${end}'::DATE`;
+ }
+ return `AND tl.created_at::DATE <= NOW()::DATE`;
+ }
+
+ protected static getDateRangeClauseMembers(key: string, dateRange: string[], tableAlias: string) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+
+ if (start === end) {
+ return `AND ${tableAlias}.created_at::DATE = '${start}'::DATE`;
+ }
+
+ return `AND ${tableAlias}.created_at::DATE >= '${start}'::DATE AND ${tableAlias}.created_at < '${end}'::DATE + INTERVAL '1 day'`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 day')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 week')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '1 month')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `AND ${tableAlias}.created_at >= (CURRENT_DATE - INTERVAL '3 months')::DATE AND ${tableAlias}.created_at < CURRENT_DATE::DATE + INTERVAL '1 day'`;
+
+ return "";
+ }
+
+ private static formatDuration(duration: moment.Duration) {
+ const empty = "0h 0m";
+ let format = "";
+
+ if (duration.asMilliseconds() === 0) return empty;
+
+ const h = ~~(duration.asHours());
+ const m = duration.minutes();
+ const s = duration.seconds();
+
+ if (h === 0 && s > 0) {
+ format = `${m}m ${s}s`;
+ } else if (h > 0 && s === 0) {
+ format = `${h}h ${m}m`;
+ } else if (h > 0 && s > 0) {
+ format = `${h}h ${m}m ${s}s`;
+ } else {
+ format = `${h}h ${m}m`;
+ }
+
+ return format;
+ }
+
+ @HandleExceptions()
+ public static async getReportingMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery, size, offset } = this.toPaginationOptions(req.query, ["name"]);
+ const { duration, date_range } = req.query;
+ const archived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const teamsClause =
+ req.query.teams as string
+ ? `AND tmiv.team_id IN (${this.flatString(req.query.teams as string)})`
+ : "";
+
+ const teamId = this.getCurrentTeamId(req);
+ const result = await this.getMembers(teamId as string, searchQuery, size, offset, teamsClause, duration as string, dateRange, archived, req.user?.id as string);
+ const body = {
+ total: result.total,
+ members: result.members,
+ team: {
+ id: req.user?.team_id,
+ name: req.user?.team_name
+ }
+ };
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ public static formatDurationDate = (date: Date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ };
+
+ @HandleExceptions()
+ public static async export(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const { search } = req.body;
+ const { duration, date_range } = req.query;
+ const archived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const teamId = this.getCurrentTeamId(req);
+ const teamName = (req.query.team_name as string)?.trim() || null;
+ const result = await this.getMembers(teamId as string, "", null, null, "", duration as string, dateRange, archived, req.user?.id as string);
+
+ let start = "-";
+ let end = "-";
+
+ if (dateRange.length === 2) {
+ start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
+ end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
+ } else {
+ switch (duration) {
+ case DATE_RANGES.YESTERDAY:
+ start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_WEEK:
+ start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_MONTH:
+ start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_QUARTER:
+ start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
+ break;
+ }
+ end = moment().format("YYYY-MM-DD").toString();
+ }
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${teamName} members - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Members");
+
+ sheet.columns = [
+ { header: "Member", key: "name", width: 30 },
+ { header: "Email", key: "email", width: 20 },
+ { header: "Tasks Assigned", key: "tasks", width: 20 },
+ { header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
+ { header: "Completed Tasks", key: "completed_tasks", width: 20 },
+ { header: "Ongoing Tasks", key: "ongoing_tasks", width: 20 },
+ { header: "Done Tasks(%)", key: "done_tasks", width: 20 },
+ { header: "Doing Tasks(%)", key: "doing_tasks", width: 20 },
+ { header: "Todo Tasks(%)", key: "todo_tasks", width: 20 }
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Members from ${teamName}`;
+ sheet.mergeCells("A1:K1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:K2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(5).values = ["Member", "Email", "Tasks Assigned", "Overdue Tasks", "Completed Tasks", "Ongoing Tasks", "Done Tasks(%)", "Doing Tasks(%)", "Todo Tasks(%)"];
+ sheet.getRow(5).font = { bold: true };
+
+ for (const member of result.members) {
+ sheet.addRow({
+ name: member.name,
+ email: member.email,
+ tasks: member.tasks,
+ overdue_tasks: member.overdue,
+ completed_tasks: member.completed,
+ ongoing_tasks: member.ongoing,
+ done_tasks: member.completed,
+ doing_tasks: member.ongoing_by_activity_logs,
+ todo_tasks: member.todo_by_activity_logs
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+ @HandleExceptions()
+ public static async exportTimeLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+
+ const { duration, date_range, team_id, team_member_id } = req.query;
+
+ const includeArchived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "twl");
+ const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_work_log");
+ const memberName = (req.query.member_name as string)?.trim() || null;
+
+ const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string);
+
+ let start = "-";
+ let end = "-";
+
+ if (dateRange.length === 2) {
+ start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
+ end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
+ } else {
+ switch (duration) {
+ case DATE_RANGES.YESTERDAY:
+ start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_WEEK:
+ start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_MONTH:
+ start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_QUARTER:
+ start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
+ break;
+ }
+ end = moment().format("YYYY-MM-DD").toString();
+ }
+
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${memberName} timelogs - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Members");
+
+ sheet.columns = [
+ { header: "Date", key: "date", width: 30 },
+ { header: "Log", key: "log", width: 120 },
+ ];
+
+ sheet.getCell("A1").value = `Timelogs of ${memberName}`;
+ sheet.mergeCells("A1:K1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:K2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(5).values = ["Time Logs"];
+ sheet.getRow(5).font = { bold: true };
+
+ for (const row of logGroups) {
+ for (const log of row.logs) {
+ sheet.addRow({
+ date: row.log_day,
+ log: `Logged ${log.time_spent_string} for ${log.task_name} in ${log.project_name}`
+ });
+ }
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+
+ }
+
+ @HandleExceptions()
+ public static async exportActivityLogs(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+
+ const { duration, date_range, team_id, team_member_id } = req.query;
+ const includeArchived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "tal");
+ const minMaxDateClause = this.getMinMaxDates(duration as string || DATE_RANGES.LAST_WEEK, dateRange, "task_activity_logs");
+ const memberName = (req.query.member_name as string)?.trim() || null;
+
+ const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id as string, team_member_id as string, includeArchived, req.user?.id as string);
+
+ let start = "-";
+ let end = "-";
+
+ if (dateRange.length === 2) {
+ start = dateRange[0] ? this.formatDurationDate(new Date(dateRange[0])).toString() : "-";
+ end = dateRange[1] ? this.formatDurationDate(new Date(dateRange[1])).toString() : "-";
+ } else {
+ switch (duration) {
+ case DATE_RANGES.YESTERDAY:
+ start = moment().subtract(1, "day").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_WEEK:
+ start = moment().subtract(1, "week").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_MONTH:
+ start = moment().subtract(1, "month").format("YYYY-MM-DD").toString();
+ break;
+ case DATE_RANGES.LAST_QUARTER:
+ start = moment().subtract(3, "months").format("YYYY-MM-DD").toString();
+ break;
+ }
+ end = moment().format("YYYY-MM-DD").toString();
+ }
+
+
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${memberName} activitylogs - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Members");
+
+ sheet.columns = [
+ { header: "Date", key: "date", width: 30 },
+ { header: "Log", key: "log", width: 120 },
+ ];
+
+ sheet.getCell("A1").value = `Activities of ${memberName}`;
+ sheet.mergeCells("A1:K1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:K2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(5).values = ["Activity Logs"];
+ sheet.getRow(5).font = { bold: true };
+
+ for (const row of logGroups) {
+ for (const log of row.logs) {
+ !log.previous ? log.previous = "NULL" : log.previous;
+ !log.current ? log.current = "NULL" : log.current;
+ switch (log.attribute_type) {
+ case "start_date":
+ log.attribute_type = "Start Date";
+ break;
+ case "end_date":
+ log.attribute_type = "End Date";
+ break;
+ case "status":
+ log.attribute_type = "Status";
+ break;
+ case "priority":
+ log.attribute_type = "Priority";
+ break;
+ case "phase":
+ log.attribute_type = "Phase";
+ break;
+ default:
+ break;
+ }
+ sheet.addRow({
+ date: row.log_day,
+ log: `Updated ${log.attribute_type} from ${log.previous} to ${log.current} in ${log.task_name} within ${log.project_name}.`
+ });
+ }
+ }
+
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+
+ public static async getMemberProjectsData(teamId: string, teamMemberId: string, searchQuery: string, archived: boolean, userId: string) {
+
+ const teamClause = teamId
+ ? `team_member_id = '${teamMemberId as string}'`
+ : `team_member_id IN (SELECT team_member_id
+ FROM team_member_info_view tmiv
+ WHERE email = (SELECT email
+ FROM team_member_info_view tmiv2
+ WHERE tmiv2.team_member_id = '${teamMemberId}' AND in_organization(p.team_id, tmiv2.team_id)))`;
+
+ const archivedClause = archived ? `` : ` AND pm.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = pm.project_id AND user_id = '${userId}')`;
+
+ const q = `SELECT p.id, p.name, pm.team_member_id,
+ (SELECT name FROM teams WHERE id = p.team_id) AS team,
+ (SELECT COUNT(task_id)
+ FROM tasks_assignees ta
+ WHERE pm.team_member_id = ta.team_member_id
+ AND task_id IN (SELECT id FROM tasks t WHERE t.project_id = pm.project_id))::INT AS task_count,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE archived IS FALSE
+ AND project_id = pm.project_id
+ AND CASE
+ WHEN (TRUE IS TRUE) THEN project_id IS NOT NULL
+ ELSE archived IS FALSE END)::INT AS project_task_count,
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
+ WHERE project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND is_completed(t.status_id, t.project_id) IS TRUE)::INT AS completed,
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
+ WHERE project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND is_overdue(t.status_id) IS TRUE)::INT AS overdue,
+ (SELECT COUNT(*)
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON ta.task_id = t.id
+ WHERE project_id = pm.project_id
+ AND ta.team_member_id = pm.team_member_id
+ AND is_completed(t.status_id, t.project_id) IS FALSE)::INT AS incompleted,
+ (SELECT SUM(time_spent)
+ FROM task_work_log twl
+ WHERE task_id IN (SELECT id FROM tasks WHERE tasks.project_id = pm.project_id)
+ AND user_id = (SELECT user_id FROM team_member_info_view tmiv WHERE pm.team_member_id = tmiv.team_member_id)) AS time_logged
+ FROM project_members pm
+ LEFT JOIN projects p ON p.id = pm.project_id
+ WHERE ${teamClause} ${searchQuery} ${archivedClause}
+ ORDER BY name;`;
+ const result = await db.query(q, []);
+
+ for (const project of result.rows) {
+ project.time_logged = formatDuration(moment.duration(project.time_logged, "seconds"));
+ project.contribution = project.project_task_count > 0 ? ((project.task_count / project.project_task_count) * 100).toFixed(0) : 0;
+ }
+
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { searchQuery } = this.toPaginationOptions(req.query, ["p.name"]);
+ const { teamMemberId, teamId } = req.query;
+ const archived = req.query.archived === "true";
+
+ const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, searchQuery, archived, req.user?.id as string);
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+
+ protected static getMinMaxDates(key: string, dateRange: string[], tableName: string) {
+ if (dateRange.length === 2) {
+ const start = moment(dateRange[0]).format("YYYY-MM-DD");
+ const end = moment(dateRange[1]).format("YYYY-MM-DD");
+ return `,(SELECT '${start}'::DATE )AS start_date, (SELECT '${end}'::DATE )AS end_date`;
+ }
+
+ if (key === DATE_RANGES.YESTERDAY)
+ return `,(SELECT (CURRENT_DATE - INTERVAL '1 day')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
+ if (key === DATE_RANGES.LAST_WEEK)
+ return `,(SELECT (CURRENT_DATE - INTERVAL '1 week')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
+ if (key === DATE_RANGES.LAST_MONTH)
+ return `,(SELECT (CURRENT_DATE - INTERVAL '1 month')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
+ if (key === DATE_RANGES.LAST_QUARTER)
+ return `,(SELECT (CURRENT_DATE - INTERVAL '3 months')::DATE) AS start_date, (SELECT (CURRENT_DATE)::DATE) AS end_date`;
+ if (key === DATE_RANGES.ALL_TIME)
+ return `,(SELECT (MIN(created_at)::DATE) FROM ${tableName} WHERE task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1))) AS start_date, (SELECT (MAX(created_at)::DATE) FROM ${tableName} WHERE task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1))) AS end_date`;
+
+ return "";
+ }
+
+
+
+ @HandleExceptions()
+ public static async getMemberActivities(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { team_member_id, team_id, duration, date_range, archived } = req.body;
+
+ const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "tal");
+ const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_activity_logs");
+
+ const logGroups = await this.memberActivityLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string);
+
+ return res.status(200).send(new ServerResponse(true, logGroups));
+ }
+
+ private static async formatLog(result: { start_date: string, end_date: string, time_logs: any[] }) {
+
+ result.time_logs.forEach((row) => {
+ const duration = moment.duration(row.time_spent, "seconds");
+ row.time_spent_string = this.formatDuration(duration);
+ });
+
+ return result;
+ }
+
+ private static async getTimeLogDays(result: { start_date: string, end_date: string, time_logs: any[] }) {
+ if (result) {
+ const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null;
+ const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null;
+
+ const days = [];
+ const logDayGroups = [];
+
+ while (startDate && moment(startDate).isSameOrBefore(endDate)) {
+ days.push(startDate.clone().format("YYYY-MM-DD"));
+ startDate ? startDate.add(1, "day") : null;
+ }
+
+ for (const day of days) {
+ const logsForDay = result.time_logs.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
+ if (logsForDay.length) {
+ logDayGroups.push({
+ log_day: day,
+ logs: logsForDay
+ });
+ }
+ }
+
+ return logDayGroups;
+
+ }
+ return [];
+ }
+
+ private static async getActivityLogDays(result: { start_date: string, end_date: string, activity_logs: any[] }) {
+ if (result) {
+ const startDate = moment(result.start_date).isValid() ? moment(result.start_date, "YYYY-MM-DD").clone() : null;
+ const endDate = moment(result.end_date).isValid() ? moment(result.end_date, "YYYY-MM-DD").clone() : null;
+
+ const days = [];
+ const logDayGroups = [];
+
+ while (startDate && moment(startDate).isSameOrBefore(endDate)) {
+ days.push(startDate.clone().format("YYYY-MM-DD"));
+ startDate ? startDate.add(1, "day") : null;
+ }
+
+ for (const day of days) {
+ const logsForDay = result.activity_logs.filter((log) => moment(moment(log.created_at).format("YYYY-MM-DD")).isSame(moment(day).format("YYYY-MM-DD")));
+ if (logsForDay.length) {
+ logDayGroups.push({
+ log_day: day,
+ logs: logsForDay
+ });
+ }
+ }
+
+ return logDayGroups;
+
+ }
+ return [];
+ }
+
+
+ private static async memberTimeLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived: boolean, userId: string) {
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND project_id NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`;
+
+ const q = `
+ SELECT user_id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(tl))), '[]'::JSON)
+ FROM (SELECT time_spent,
+ created_at,
+ twl.task_id AS task_id,
+ (SELECT project_id FROM tasks WHERE tasks.id = twl.task_id) AS project_id,
+ (SELECT name FROM projects WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = twl.task_id)) AS project_name,
+ (SELECT name FROM tasks WHERE tasks.id = twl.task_id) AS task_name,
+ CONCAT((SELECT key
+ FROM projects
+ WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = twl.task_id)), '-',
+ (SELECT task_no FROM tasks WHERE tasks.id = twl.task_id)) AS task_key
+ FROM task_work_log twl
+ WHERE twl.user_id = tmiv.user_id
+ ${durationClause}
+ AND task_id IN (SELECT id FROM tasks WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1) ${archivedClause} )
+ ORDER BY twl.updated_at DESC) tl) AS time_logs
+ ${minMaxDateClause}
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_id = $1
+ AND tmiv.team_member_id = $2
+ `;
+
+ const result = await db.query(q, [team_id, team_member_id]);
+
+ let logGroups: any[] = [];
+
+ if (result.rows.length) {
+ const [data] = result.rows;
+
+ const formattedLogs = await this.formatLog(data);
+
+ logGroups = await this.getTimeLogDays(formattedLogs);
+ }
+
+ return logGroups;
+ }
+
+ private static async memberActivityLogsData(durationClause: string, minMaxDateClause: string, team_id: string, team_member_id: string, includeArchived:boolean, userId: string) {
+
+ const archivedClause = includeArchived ? `` : `AND (SELECT project_id FROM tasks WHERE id = tal.task_id) NOT IN (SELECT project_id FROM archived_projects WHERE archived_projects.user_id = '${userId}')`;
+
+ const q = `
+ SELECT user_id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(al))), '[]'::JSON)
+ FROM (SELECT task_id,
+ user_id,
+ (SELECT project_id FROM tasks WHERE id = tal.task_id) AS project_id,
+ tal.task_id AS task_id,
+ (SELECT name FROM projects WHERE id = (SELECT project_id FROM tasks WHERE id = tal.task_id)) AS project_name,
+ (SELECT name FROM tasks WHERE tasks.id = tal.task_id) AS task_name,
+ CONCAT((SELECT key
+ FROM projects
+ WHERE id = (SELECT project_id FROM tasks WHERE tasks.id = tal.task_id)), '-',
+ (SELECT task_no FROM tasks WHERE tasks.id = tal.task_id)) AS task_key,
+ created_at,
+ attribute_type,
+ log_type,
+ (CASE
+ WHEN (attribute_type = 'status' AND old_value <> 'Unmapped')
+ THEN (SELECT name FROM task_statuses WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'priority' AND old_value <> 'Unmapped')
+ THEN (SELECT name FROM task_priorities WHERE id = old_value::UUID)
+ WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
+ THEN (SELECT name FROM project_phases WHERE id = old_value::UUID)
+ ELSE (old_value) END) AS previous,
+
+ (CASE
+ WHEN (attribute_type = 'status' AND new_value <> 'Unmapped')
+ THEN (SELECT name FROM task_statuses WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'priority' AND new_value <> 'Unmapped')
+ THEN (SELECT name FROM task_priorities WHERE id = new_value::UUID)
+ WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
+ THEN (SELECT name FROM project_phases WHERE id = new_value::UUID)
+ ELSE (new_value) END) AS current,
+
+ (CASE
+ WHEN (attribute_type = 'status' AND old_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_statuses WHERE id = old_value::UUID),
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = old_value::UUID))) rec)
+ ELSE (NULL) END) AS previous_status,
+
+ (CASE
+ WHEN (attribute_type = 'status' AND new_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_statuses WHERE id = new_value::UUID),
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = new_value::UUID))) rec)
+ ELSE (NULL) END) AS next_status,
+
+ (CASE
+ WHEN (attribute_type = 'priority' AND old_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_priorities WHERE id = old_value::UUID),
+ (SELECT color_code FROM task_priorities WHERE id = old_value::UUID)) rec)
+ ELSE (NULL) END) AS previous_priority,
+
+ (CASE
+ WHEN (attribute_type = 'priority' AND new_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM task_priorities WHERE id = new_value::UUID),
+ (SELECT color_code FROM task_priorities WHERE id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS next_priority,
+
+ (CASE
+ WHEN (attribute_type = 'phase' AND old_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM project_phases WHERE id = old_value::UUID),
+ (SELECT color_code FROM project_phases WHERE id = old_value::UUID)) rec)
+ ELSE (NULL) END) AS previous_phase,
+
+ (CASE
+ WHEN (attribute_type = 'phase' AND new_value <> 'Unmapped')
+ THEN (SELECT ROW_TO_JSON(rec)
+ FROM (SELECT (SELECT name FROM project_phases WHERE id = new_value::UUID),
+ (SELECT color_code FROM project_phases WHERE id = new_value::UUID)) rec)
+ ELSE (NULL) END) AS next_phase
+
+ FROM task_activity_logs tal
+ WHERE tal.user_id = tmiv.user_id
+ ${durationClause}
+ AND tal.team_id = $1 AND tal.attribute_type IN ('status', 'priority', 'phase', 'end_date', 'start_date')
+ ${archivedClause}
+ ORDER BY created_at DESC) al) AS activity_logs
+ ${minMaxDateClause}
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_id = $1
+ AND tmiv.team_member_id = $2
+ `;
+
+ const result = await db.query(q, [team_id, team_member_id]);
+
+ let logGroups: any[] = [];
+
+ if (result.rows.length) {
+ const [data] = result.rows;
+
+ logGroups = await this.getActivityLogDays(data);
+ }
+
+ return logGroups;
+
+ }
+
+ @HandleExceptions()
+ public static async getMemberTimelogs(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const { team_member_id, team_id, duration, date_range, archived } = req.body;
+
+ const durationClause = ReportingMembersController.getDateRangeClauseMembers(duration || DATE_RANGES.LAST_WEEK, date_range, "twl");
+ const minMaxDateClause = this.getMinMaxDates(duration || DATE_RANGES.LAST_WEEK, date_range, "task_work_log");
+
+ const logGroups = await this.memberTimeLogsData(durationClause, minMaxDateClause, team_id, team_member_id, archived, req.user?.id as string);
+
+ return res.status(200).send(new ServerResponse(true, logGroups));
+ }
+
+ @HandleExceptions()
+ public static async getMemberTaskStats(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const { duration, date_range, team_member_id } = req.query;
+ const includeArchived = req.query.archived === "true";
+
+ let dateRange: string[] = [];
+ if (typeof date_range === "string") {
+ dateRange = date_range.split(",");
+ }
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND t.project_id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = t.project_id AND archived_projects.user_id = '${req.user?.id}')`;
+
+
+ const assignClause = this.memberAssignDurationFilter(duration as string, dateRange);
+ const completedDurationClasue = this.completedDurationFilter(duration as string, dateRange);
+ const overdueClauseByDate = this.getActivityLogsOverdue(duration as string, dateRange);
+ const taskSelectorClause = this.getTaskSelectorClause();
+
+ const q = `
+ SELECT name AS team_member_name,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(assigned))), '[]')
+ FROM (${taskSelectorClause}
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${assignClause} ${archivedClause}) assigned) AS assigned,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(completed))), '[]')
+ FROM (${taskSelectorClause}
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1
+ AND is_completed(status_id, t.project_id)
+ ${completedDurationClasue} ${archivedClause}) completed) AS completed,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(ongoing))), '[]')
+ FROM (${taskSelectorClause}
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1
+ AND is_doing(status_id, t.project_id) ${archivedClause}) ongoing) AS ongoing,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(overdue))), '[]')
+ FROM (${taskSelectorClause}
+ FROM tasks t
+ LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
+ WHERE ta.team_member_id = $1 ${overdueClauseByDate} ${archivedClause}) overdue) AS overdue
+
+ FROM team_member_info_view WHERE team_member_id = $1;
+ `;
+
+ const result = await db.query(q, [team_member_id]);
+ const [data] = result.rows;
+
+ if (data) {
+ for (const taskArray of [data.assigned, data.completed, data.ongoing, data.overdue]) {
+ this.updateTaskProperties(taskArray);
+ }
+ }
+
+ const body = {
+ team_member_name: data.team_member_name,
+ groups: [
+ {
+ name: "Tasks Assigned",
+ color_code: "#7590c9",
+ tasks: data.assigned ? data.assigned : 0
+ },
+ {
+ name: "Tasks Completed",
+ color_code: "#75c997",
+ tasks: data.completed ? data.completed : 0
+ },
+ {
+ name: "Tasks Overdue",
+ color_code: "#eb6363",
+ tasks: data.overdue ? data.overdue : 0
+ },
+ {
+ name: "Tasks Ongoing",
+ color_code: "#7cb5ec",
+ tasks: data.ongoing ? data.ongoing : 0
+ }
+ ]
+ };
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ private static updateTaskProperties(tasks: any[]) {
+ for (const task of tasks) {
+ task.project_color = getColor(task.project_name);
+ task.estimated_string = formatDuration(moment.duration(~~(task.total_minutes), "seconds"));
+ task.time_spent_string = formatDuration(moment.duration(~~(task.time_logged), "seconds"));
+ task.overlogged_time_string = formatDuration(moment.duration(~~(task.overlogged_time), "seconds"));
+ task.overdue_days = task.days_overdue ? task.days_overdue : null;
+ }
+}
+
+@HandleExceptions()
+public static async getSingleMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const { team_member_id } = req.query;
+ const includeArchived = req.query.archived === "true";
+
+ const archivedClause = includeArchived
+ ? ""
+ : `AND projects.id NOT IN (SELECT project_id FROM archived_projects WHERE project_id = projects.id AND archived_projects.user_id = '${req.user?.id}')`;
+
+ const q = `SELECT id,
+ name,
+ color_code,
+ start_date,
+ end_date,
+
+ (SELECT icon FROM sys_project_statuses WHERE id = projects.status_id) AS status_icon,
+ (SELECT name FROM sys_project_statuses WHERE id = projects.status_id) AS status_name,
+ (SELECT color_code FROM sys_project_statuses WHERE id = projects.status_id) AS status_color,
+
+ (SELECT name FROM sys_project_healths WHERE id = projects.health_id) AS health_name,
+ (SELECT color_code FROM sys_project_healths WHERE id = projects.health_id) AS health_color,
+
+ (SELECT name FROM project_categories WHERE id = projects.category_id) AS category_name,
+ (SELECT color_code
+ FROM project_categories
+ WHERE id = projects.category_id) AS category_color,
+
+ (SELECT COALESCE(ROW_TO_JSON(pm), '{}'::JSON)
+ FROM (SELECT team_member_id AS pm_id,
+ (SELECT COALESCE(ROW_TO_JSON(pmi), '{}'::JSON)
+ FROM (SELECT name, email, avatar_url
+ FROM team_member_info_view tmiv
+ WHERE tmiv.team_member_id = pm.team_member_id) pmi) AS project_manager_info,
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = pm.team_member_id
+ AND email_invitations.team_id = (SELECT team_id
+ FROM team_member_info_view
+ WHERE team_member_id = pm.team_member_id)) AS pending_invitation,
+ (SELECT active FROM team_members WHERE id = pm.team_member_id)
+ FROM project_members pm
+ WHERE project_id = projects.id
+ AND project_access_level_id =
+ (SELECT id FROM project_access_levels WHERE key = 'PROJECT_MANAGER')) pm) AS project_manager,
+
+ (SELECT COALESCE(SUM(total_minutes), 0)
+ FROM tasks
+ WHERE project_id = projects.id) AS estimated_time,
+
+ (SELECT SUM((SELECT COALESCE(SUM(time_spent), 0)
+ FROM task_work_log
+ WHERE task_id = tasks.id))
+ FROM tasks
+ WHERE project_id = projects.id) AS actual_time,
+
+ (SELECT name FROM team_member_info_view WHERE team_member_id = $1) As team_member_name
+
+ FROM projects
+ WHERE projects.id IN (SELECT project_id FROM project_members WHERE team_member_id = $1) ${archivedClause};`;
+
+ const result = await db.query(q, [team_member_id]);
+ const data = result.rows;
+
+ for (const row of data) {
+ row.estimated_time = int(row.estimated_time);
+ row.actual_time = int(row.actual_time);
+ row.estimated_time_string = this.convertMinutesToHoursAndMinutes(int(row.estimated_time));
+ row.actual_time_string = this.convertSecondsToHoursAndMinutes(int(row.actual_time));
+ row.days_left = ReportingControllerBase.getDaysLeft(row.end_date);
+ row.is_overdue = ReportingControllerBase.isOverdue(row.end_date);
+ if (row.days_left && row.is_overdue) {
+ row.days_left = row.days_left.toString().replace(/-/g, "");
+ }
+ row.is_today = this.isToday(row.end_date);
+ if (row.project_manager) {
+ row.project_manager.name = row.project_manager.project_manager_info.name;
+ row.project_manager.avatar_url = row.project_manager.project_manager_info.avatar_url;
+ row.project_manager.color_code = getColor(row.project_manager.name);
+ }
+ row.project_health = row.health_name ? row.health_name : null;
+ }
+
+ const body = {
+ team_member_name: data[0].team_member_name,
+ projects: data
+ };
+
+ return res.status(200).send(new ServerResponse(true, body));
+
+}
+
+ @HandleExceptions()
+ public static async exportMemberProjects(req: IWorkLenzRequest, res: IWorkLenzResponse) {
+ const teamMemberId = (req.query.team_member_id as string)?.trim() || null;
+ const teamId = (req.query.team_id as string)?.trim() || null;
+ const memberName = (req.query.team_member_name as string)?.trim() || null;
+ const teamName = (req.query.team_name as string)?.trim() || "";
+ const archived = req.query.archived === "true";
+
+ const result = await this.getMemberProjectsData(teamId as string, teamMemberId as string, "", archived, req.user?.id as string);
+
+ // excel file
+ const exportDate = moment().format("MMM-DD-YYYY");
+ const fileName = `${memberName} projects - ${exportDate}`;
+ const workbook = new Excel.Workbook();
+
+ const sheet = workbook.addWorksheet("Projects");
+
+ sheet.columns = [
+ { header: "Project", key: "project", width: 30 },
+ { header: "Team", key: "team", width: 20 },
+ { header: "Tasks", key: "tasks", width: 20 },
+ { header: "Contribution(%)", key: "contribution", width: 20 },
+ { header: "Incompleted Tasks", key: "incompleted_tasks", width: 20 },
+ { header: "Completed Tasks", key: "completed_tasks", width: 20 },
+ { header: "Overdue Tasks", key: "overdue_tasks", width: 20 },
+ { header: "Logged Time", key: "logged_time", width: 20 }
+ ];
+
+ // set title
+ sheet.getCell("A1").value = `Projects of ${memberName} - ${teamName}`;
+ sheet.mergeCells("A1:H1");
+ sheet.getCell("A1").alignment = { horizontal: "center" };
+ sheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } };
+ sheet.getCell("A1").font = { size: 16 };
+
+ // set export date
+ sheet.getCell("A2").value = `Exported on : ${exportDate}`;
+ sheet.mergeCells("A2:H2");
+ sheet.getCell("A2").alignment = { horizontal: "center" };
+ sheet.getCell("A2").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "F2F2F2" } };
+ sheet.getCell("A2").font = { size: 12 };
+
+ // set duration
+ // const start = 'duartion start';
+ // const end = 'duartion end';
+ // sheet.getCell("A3").value = `From : ${start} To : ${end}`;
+ // sheet.mergeCells("A3:D3");
+
+ // set table headers
+ sheet.getRow(4).values = ["Project", "Team", "Tasks", "Contribution(%)", "Incompleted Tasks", "Completed Tasks", "Overdue Tasks", "Logged Time"];
+ sheet.getRow(4).font = { bold: true };
+
+ for (const project of result) {
+ sheet.addRow({
+ project: project.name,
+ team: project.team,
+ tasks: project.task_count ? project.task_count.toString() : "-",
+ contribution: project.contribution ? project.contribution.toString() : "-",
+ incompleted_tasks: project.incompleted ? project.incompleted.toString() : "-",
+ completed_tasks: project.completed ? project.completed.toString() : "-",
+ overdue_tasks: project.overdue ? project.overdue.toString() : "-",
+ logged_time: project.time_logged ? project.time_logged.toString() : "-"
+ });
+ }
+
+ // download excel
+ res.setHeader("Content-Type", "application/vnd.openxmlformats");
+ res.setHeader("Content-Disposition", `attachment; filename=${fileName}.xlsx`);
+
+ await workbook.xlsx.write(res)
+ .then(() => {
+ res.end();
+ });
+
+ }
+
+}
diff --git a/worklenz-backend/src/controllers/resource-allocation-controller.ts b/worklenz-backend/src/controllers/resource-allocation-controller.ts
new file mode 100644
index 00000000..93d38e7c
--- /dev/null
+++ b/worklenz-backend/src/controllers/resource-allocation-controller.ts
@@ -0,0 +1,82 @@
+import moment from "moment";
+
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getDatesForResourceAllocation, getWeekRange} from "../shared/tasks-controller-utils";
+import {getColor} from "../shared/utils";
+
+
+export default class ResourceallocationController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getProjectWiseResources(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {start, end} = req.query;
+
+ const dates = await getDatesForResourceAllocation(start as string, end as string);
+ const months = await getWeekRange(dates);
+
+ const q = `SELECT get_project_wise_resources($1, $2, $3) as resources;`;
+ const result = await db.query(q, [start, moment(dates.length && dates.at(-1)?.date).format("YYYY-MM-DD") || end, req.user?.team_id || null]);
+ const [data] = result.rows;
+
+ const scheduleData = JSON.parse(data.resources);
+
+ for (const element of scheduleData) {
+ for (const schedule of element.schedule) {
+ const min = dates.findIndex((date) => moment(schedule.date_series).isSame(date.date, "days")) || 0;
+ schedule.min = min + 1;
+ }
+
+ for (const task of element.unassigned_tasks) {
+ const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
+ task.min = min + 1;
+ }
+
+ for (const member of element.project_members) {
+ for (const task of member.tasks) {
+ const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
+ task.min = min + 1;
+ }
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, {projects: scheduleData, dates, months}));
+ }
+
+ @HandleExceptions()
+ public static async getUserWiseResources(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const {start, end} = req.query;
+
+ const dates = await getDatesForResourceAllocation(start as string, end as string);
+ const months = await getWeekRange(dates);
+
+ const q = `SELECT get_team_wise_resources($1, $2, $3) as resources;`;
+ const result = await db.query(q, [start, moment(dates.length && dates.at(-1)?.date).format("YYYY-MM-DD") || end, req.user?.team_id || null]);
+ const [data] = result.rows;
+
+ const scheduleData = JSON.parse(data.resources);
+
+ const obj = [];
+
+ for (const element of scheduleData) {
+ element.color_code = getColor(element.name);
+ for (const schedule of element.schedule) {
+ const min = dates.findIndex((date) => moment(schedule.date_series).isSame(date.date, "days")) || 0;
+ schedule.min = min + 1;
+ }
+
+ for (const member of element.project_members) {
+ for (const task of member.tasks) {
+ const min = dates.findIndex((date) => moment(task.date_series).isSame(date.date, "days")) || 0;
+ task.min = min + 1;
+ }
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, {projects: scheduleData, dates, months}));
+ }
+}
diff --git a/worklenz-backend/src/controllers/schedule/schedule-controller-base.ts b/worklenz-backend/src/controllers/schedule/schedule-controller-base.ts
new file mode 100644
index 00000000..ed259e38
--- /dev/null
+++ b/worklenz-backend/src/controllers/schedule/schedule-controller-base.ts
@@ -0,0 +1,64 @@
+import { PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import WorklenzControllerBase from ".././worklenz-controller-base";
+
+export const GroupBy = {
+ STATUS: "status",
+ PRIORITY: "priority",
+ LABELS: "labels",
+ PHASE: "phase"
+};
+
+export interface IScheduleTaskGroup {
+ id?: string;
+ name: string;
+ color_code: string;
+ category_id: string | null;
+ old_category_id?: string;
+ tasks: any[];
+ isExpand: boolean;
+}
+
+export default class ScheduleTasksControllerBase extends WorklenzControllerBase {
+ protected static calculateTaskCompleteRatio(totalCompleted: number, totalTasks: number) {
+ if (totalCompleted === 0 && totalTasks === 0) return 0;
+ const ratio = ((totalCompleted / totalTasks) * 100);
+ return ratio == Infinity ? 100 : ratio.toFixed();
+ }
+
+ public static updateTaskViewModel(task: any) {
+ task.progress = ~~(task.total_minutes_spent / task.total_minutes * 100);
+ task.overdue = task.total_minutes < task.total_minutes_spent;
+
+ if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0";
+
+ task.is_sub_task = !!task.parent_task_id;
+
+ task.name_color = getColor(task.name);
+ task.priority_color = PriorityColorCodes[task.priority_value] || PriorityColorCodes["0"];
+ task.show_sub_tasks = false;
+
+ if (task.phase_id) {
+ task.phase_color = task.phase_name
+ ? getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA
+ : null;
+ }
+
+ if (Array.isArray(task.assignees)) {
+ for (const assignee of task.assignees) {
+ assignee.color_code = getColor(assignee.name);
+ }
+ }
+
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
+
+ const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed;
+ const totalTasks = +task.sub_tasks_count + 1; // +1 for parent
+ task.complete_ratio = ScheduleTasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks);
+ task.completed_count = totalCompleted;
+ task.total_tasks_count = totalTasks;
+
+ return task;
+ }
+}
diff --git a/worklenz-backend/src/controllers/schedule/schedule-controller.ts b/worklenz-backend/src/controllers/schedule/schedule-controller.ts
new file mode 100644
index 00000000..212145b5
--- /dev/null
+++ b/worklenz-backend/src/controllers/schedule/schedule-controller.ts
@@ -0,0 +1,945 @@
+import db from "../../config/db";
+import { ParsedQs } from "qs";
+import HandleExceptions from "../../decorators/handle-exceptions";
+import { IWorkLenzRequest } from "../../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../../interfaces/worklenz-response";
+import { ServerResponse } from "../../models/server-response";
+import { TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA, UNMAPPED } from "../../shared/constants";
+import { getColor } from "../../shared/utils";
+import moment, { Moment } from "moment";
+import momentTime from "moment-timezone";
+import ScheduleTasksControllerBase, { GroupBy, IScheduleTaskGroup } from "./schedule-controller-base";
+
+interface IDateUnions {
+ date_union: {
+ start_date: string | null;
+ end_date: string | null;
+ },
+ logs_date_union: {
+ start_date: string | null;
+ end_date: string | null;
+ },
+ allocated_date_union: {
+ start_date: string | null;
+ end_date: string | null;
+ }
+}
+
+interface IDatesPair {
+ start_date: string | null,
+ end_date: string | null
+}
+
+export class IScheduleTaskListGroup implements IScheduleTaskGroup {
+ name: string;
+ category_id: string | null;
+ color_code: string;
+ tasks: any[];
+ isExpand: boolean;
+
+ constructor(group: any) {
+ this.name = group.name;
+ this.category_id = group.category_id || null;
+ this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA;
+ this.tasks = [];
+ this.isExpand = group.isExpand;
+ }
+}
+
+export default class ScheduleControllerV2 extends ScheduleTasksControllerBase {
+
+ private static GLOBAL_DATE_WIDTH = 35;
+ private static GLOBAL_START_DATE = moment().format("YYYY-MM-DD");
+ private static GLOBAL_END_DATE = moment().format("YYYY-MM-DD");
+
+ // Migrate data
+ @HandleExceptions()
+ public static async migrate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const getDataq = `SELECT p.id,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT tmiv.team_member_id,
+ tmiv.user_id,
+
+ LEAST(
+ (SELECT MIN(LEAST(start_date, end_date)) AS start_date
+ FROM tasks
+ INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE archived IS FALSE
+ AND project_id = p.id
+ AND ta.team_member_id = tmiv.team_member_id),
+ (SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_start_date
+ FROM task_work_log twl
+ INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
+ WHERE t.project_id = p.id
+ AND twl.user_id = tmiv.user_id)
+ ) AS lowest_date,
+
+ GREATEST(
+ (SELECT MAX(GREATEST(start_date, end_date)) AS end_date
+ FROM tasks
+ INNER JOIN tasks_assignees ta ON tasks.id = ta.task_id
+ WHERE archived IS FALSE
+ AND project_id = p.id
+ AND ta.team_member_id = tmiv.team_member_id),
+ (SELECT MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS ll_end_date
+ FROM task_work_log twl
+ INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
+ WHERE t.project_id = p.id
+ AND twl.user_id = tmiv.user_id)
+ ) AS greatest_date
+
+ FROM project_members pm
+ INNER JOIN team_member_info_view tmiv
+ ON pm.team_member_id = tmiv.team_member_id
+ WHERE project_id = p.id) rec) AS members
+
+FROM projects p
+WHERE team_id IS NOT NULL
+AND p.id NOT IN (SELECT project_id FROM archived_projects)`;
+
+ const projectMembersResults = await db.query(getDataq);
+
+ const projectMemberData = projectMembersResults.rows;
+
+ const arrayToInsert = [];
+
+ for (const data of projectMemberData) {
+ if (data.members.length) {
+ for (const member of data.members) {
+
+ const body = {
+ project_id: data.id,
+ team_member_id: member.team_member_id,
+ allocated_from: member.lowest_date ? member.lowest_date : null,
+ allocated_to: member.greatest_date ? member.greatest_date : null
+ };
+
+ if (body.allocated_from && body.allocated_to) arrayToInsert.push(body);
+
+ }
+ }
+ }
+
+ const insertArray = JSON.stringify(arrayToInsert);
+
+ const insertFunctionCall = `SELECT migrate_member_allocations($1)`;
+ await db.query(insertFunctionCall, [insertArray]);
+
+ return res.status(200).send(new ServerResponse(true, ""));
+ }
+
+
+ private static async getFirstLastDates(teamId: string, userId: string) {
+ const q = `SELECT MIN(LEAST(allocated_from, allocated_to)) AS start_date,
+ MAX(GREATEST(allocated_from, allocated_to)) AS end_date,
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(start_date) AS min_date, MAX(start_date) AS max_date
+ FROM tasks
+ WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)
+ AND project_id NOT IN
+ (SELECT project_id
+ FROM archived_projects
+ WHERE user_id = $2)
+ AND tasks.archived IS FALSE
+ UNION
+ SELECT MIN(end_date) AS min_date, MAX(end_date) AS max_date
+ FROM tasks
+ WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)
+ AND project_id NOT IN
+ (SELECT project_id
+ FROM archived_projects
+ WHERE user_id = $2)
+ AND tasks.archived IS FALSE) AS dates) rec) AS date_union,
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS start_date,
+ MAX(twl.created_at - INTERVAL '1 second' * twl.time_spent) AS end_date
+ FROM task_work_log twl
+ INNER JOIN tasks t ON twl.task_id = t.id AND t.archived IS FALSE
+ WHERE t.project_id IN (SELECT id FROM projects WHERE team_id = $1)
+ AND project_id NOT IN
+ (SELECT project_id
+ FROM archived_projects
+ WHERE user_id = $2)) rec) AS logs_date_union
+ FROM project_member_allocations
+ WHERE project_id IN (SELECT id FROM projects WHERE team_id = $1)`;
+
+ const res = await db.query(q, [teamId, userId]);
+ return res.rows[0];
+ }
+
+ private static validateEndDate(endDate: Moment): boolean {
+ return endDate.isBefore(moment(), "day");
+ }
+
+ private static validateStartDate(startDate: Moment): boolean {
+ return startDate.isBefore(moment(), "day");
+ }
+
+ private static getScrollAmount(startDate: Moment) {
+ const today = moment();
+ const daysDifference = today.diff(startDate, "days");
+
+ return (this.GLOBAL_DATE_WIDTH * daysDifference);
+ }
+
+ private static setAllocationIndicator(item: any) {
+ if (moment(item.allocated_from).isValid() && moment(item.allocated_to).isValid()) {
+ const daysFromStart = moment(item.allocated_from).diff(this.GLOBAL_START_DATE, "days");
+ const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
+
+ const daysDifference = moment(item.allocated_to).diff(item.allocated_from, "days");
+ const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
+
+ return { indicatorOffset, indicatorWidth };
+ }
+
+ return null;
+
+ }
+
+ private static setIndicatorWithLogIndicator(item: any) {
+
+ const daysFromStart = moment(item.start_date).diff(this.GLOBAL_START_DATE, "days");
+ const indicatorOffset = daysFromStart * this.GLOBAL_DATE_WIDTH;
+
+ const daysDifference = moment(item.end_date).diff(item.start_date, "days");
+ const indicatorWidth = (daysDifference + 1) * this.GLOBAL_DATE_WIDTH;
+
+ let logIndicatorOffset = 0;
+ let logIndicatorWidth = 0;
+
+ if (item.logs_date_union && item.logs_date_union.start_date && item.logs_date_union.end_date) {
+ const daysFromIndicatorStart = moment(item.logs_date_union.start_date).diff(item.start_date, "days");
+ logIndicatorOffset = daysFromIndicatorStart * this.GLOBAL_DATE_WIDTH;
+
+ const daysDifferenceFromIndicator = moment(item.logs_date_union.end_date).diff(item.logs_date_union.start_date, "days");
+ logIndicatorWidth = (daysDifferenceFromIndicator + 1) * this.GLOBAL_DATE_WIDTH;
+ }
+
+ const body = {
+ indicatorOffset,
+ indicatorWidth,
+ logIndicatorOffset,
+ logIndicatorWidth
+ };
+
+ return body;
+
+ }
+
+ private static async setChartStartEnd(dateRange: IDatesPair, logsRange: IDatesPair, allocatedRange: IDatesPair, timeZone: string) {
+
+ const datesToCheck = [];
+
+ const body = {
+ date_union: {
+ start_date: dateRange.start_date ? momentTime.tz(dateRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ end_date: dateRange.end_date ? momentTime.tz(dateRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ },
+ logs_date_union: {
+ start_date: logsRange.start_date ? momentTime.tz(logsRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ end_date: logsRange.end_date ? momentTime.tz(logsRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ },
+ allocated_date_union: {
+ start_date: allocatedRange.start_date ? momentTime.tz(allocatedRange.start_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ end_date: allocatedRange.end_date ? momentTime.tz(allocatedRange.end_date, `${timeZone}`).format("YYYY-MM-DD") : null,
+ }
+ };
+
+ for (const dateKey in body) {
+ if (body[dateKey as keyof IDateUnions] && body[dateKey as keyof IDateUnions].start_date) {
+ datesToCheck.push(moment(body[dateKey as keyof IDateUnions].start_date));
+ }
+ if (body[dateKey as keyof IDateUnions] && body[dateKey as keyof IDateUnions].end_date) {
+ datesToCheck.push(moment(body[dateKey as keyof IDateUnions].end_date));
+ }
+ }
+
+ const validDateToCheck = datesToCheck.filter((date) => date.isValid());
+
+ dateRange.start_date = moment.min(validDateToCheck).format("YYYY-MM-DD");
+ dateRange.end_date = moment.max(validDateToCheck).format("YYYY-MM-DD");
+
+ return dateRange;
+ }
+
+ private static async mainDateValidator(dateRange: any) {
+ const today = new Date();
+ let startDate = moment(today).clone().startOf("year");
+ let endDate = moment(today).clone().endOf("year").add(1, "year");
+
+ if (dateRange.start_date && dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("year") : moment(today).clone().startOf("year");
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("year") : moment(dateRange.end_date).endOf("year");
+ } else if (dateRange.start_date && !dateRange.end_date) {
+ startDate = this.validateStartDate(moment(dateRange.start_date)) ? moment(dateRange.start_date).startOf("year") : moment(today).clone().startOf("year");
+ } else if (!dateRange.start_date && dateRange.end_date) {
+ endDate = this.validateEndDate(moment(dateRange.end_date)) ? moment(today).clone().endOf("year") : moment(dateRange.end_date).endOf("year");
+ }
+ return { startDate, endDate, today };
+ }
+
+ private static async createDateColumns(xMonthsBeforeStart: Moment, xMonthsAfterEnd: Moment, today: Date) {
+ const dateData = [];
+ let days = -1;
+
+ const currentDate = xMonthsBeforeStart.clone();
+
+ while (currentDate.isBefore(xMonthsAfterEnd)) {
+ const monthData = {
+ month: currentDate.format("MMM YYYY"),
+ weeks: [] as number[],
+ days: [] as { day: number, name: string, isWeekend: boolean, isToday: boolean }[],
+ };
+ const daysInMonth = currentDate.daysInMonth();
+ for (let day = 1; day <= daysInMonth; day++) {
+ const dayOfMonth = currentDate.date();
+ const dayName = currentDate.format("ddd");
+ const isWeekend = [0, 6].includes(currentDate.day());
+ const isToday = moment(moment(today).format("YYYY-MM-DD")).isSame(moment(currentDate).format("YYYY-MM-DD"));
+ monthData.days.push({ day: dayOfMonth, name: dayName, isWeekend, isToday });
+ currentDate.add(1, "day");
+ days++;
+ }
+ dateData.push(monthData);
+ }
+
+ return { dateData, days };
+
+ }
+
+ @HandleExceptions()
+ public static async createDateRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const dates = await this.getFirstLastDates(req.params.id as string, req.user?.id as string);
+
+ const dateRange = dates.date_union;
+ const logsRange = dates.logs_date_union;
+ const allocatedRange = { start_date: dates.start_date, end_date: dates.end_date };
+
+ await this.setChartStartEnd(dateRange, logsRange, allocatedRange, req.query.timeZone as string);
+
+ const { startDate, endDate, today } = await this.mainDateValidator(dateRange);
+
+ const xMonthsBeforeStart = startDate.clone().subtract(3, "months");
+ const xMonthsAfterEnd = endDate.clone().add(2, "year");
+
+ this.GLOBAL_START_DATE = moment(xMonthsBeforeStart).format("YYYY-MM-DD");
+ this.GLOBAL_END_DATE = moment(xMonthsAfterEnd).format("YYYY-MM-DD");
+
+ const { dateData, days } = await this.createDateColumns(xMonthsBeforeStart, xMonthsAfterEnd, today);
+
+ const scrollBy = await this.getScrollAmount(xMonthsBeforeStart);
+
+ const result = {
+ date_data: dateData,
+ width: days + 1,
+ scroll_by: scrollBy,
+ chart_start: moment(this.GLOBAL_START_DATE).format("YYYY-MM-DD"),
+ chart_end: moment(this.GLOBAL_END_DATE).format("YYYY-MM-DD")
+ };
+
+ return res.status(200).send(new ServerResponse(true, result));
+ }
+
+ private static async getProjectsQuery(teamId: string, userId: string) {
+ const q = `SELECT p.id,
+ p.name,
+
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = p.id
+ UNION
+ SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = p.id) AS dates) rec) AS date_union,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT pm.id AS project_member_id,
+ tmiv.team_member_id,
+ tmiv.user_id,
+ name AS name,
+ avatar_url,
+ TRUE AS project_member,
+
+ EXISTS(SELECT email
+ FROM email_invitations
+ WHERE team_member_id = tmiv.team_member_id
+ AND email_invitations.team_id = $1) AS pending_invitation,
+
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT
+ pma.id,
+ pma.allocated_from,
+ pma.allocated_to
+ FROM project_member_allocations pma
+ WHERE pma.team_member_id = tmiv.team_member_id
+ AND pma.project_id = p.id) rec)
+ AS allocations
+
+ FROM project_members pm
+ INNER JOIN team_member_info_view tmiv
+ ON pm.team_member_id = tmiv.team_member_id
+ WHERE project_id = p.id
+ ORDER BY NAME ASC) rec) AS members
+
+ FROM projects p
+ WHERE team_id = $1
+ AND p.id NOT IN
+ (SELECT project_id FROM archived_projects WHERE user_id = $2)
+ ORDER BY p.name`;
+
+ const result = await db.query(q, [teamId, userId]);
+ return result;
+ }
+
+ @HandleExceptions()
+ public static async getProjects(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const userId = req.user?.id as string;
+ const teamId = req.params.id as string;
+ const timeZone = req.query.timeZone as string;
+
+ const result = await this.getProjectsQuery(teamId, userId);
+
+ for (const project of result.rows) {
+
+ const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
+
+ project.allocated_from = lowestDate ? moment(lowestDate).format("YYYY-MM-DD") : null;
+ project.allocated_to = highestDate ? moment(highestDate).format("YYYY-MM-DD") : null;
+
+ const styles = this.setAllocationIndicator(project);
+
+ project.indicator_offset = styles?.indicatorOffset && project.members.length ? styles.indicatorOffset : 0;
+ project.indicator_width = styles?.indicatorWidth && project.members.length ? styles.indicatorWidth : 0;
+
+ project.color_code = getColor(project.name);
+
+ for (const member of project.members) {
+
+ const mergedAllocation = await this.mergeAllocations(member.allocations);
+
+ member.allocations = mergedAllocation;
+
+ for (const allocation of member.allocations) {
+
+ allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
+ allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
+
+ const styles = this.setAllocationIndicator(allocation);
+
+ allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
+ allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
+
+ }
+
+ member.color_code = getColor(member.name);
+
+ }
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getSingleProjectIndicator(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const projectId = req.params.id as string;
+ const teamMemberId = req.query.team_member_id as string;
+ const timeZone = req.query.timeZone as string;
+ const projectIndicatorRefresh = req.query.isProjectRefresh;
+
+ const q = `SELECT id,
+ allocated_from,
+ allocated_to,
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1
+ UNION
+ SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1) AS dates) rec) AS date_union
+ FROM project_member_allocations
+ WHERE team_member_id = $2
+ AND project_id = $1`;
+
+ const result = await db.query(q, [projectId, teamMemberId]);
+
+ const body = {
+ project_allocation: { start_date: null, end_date: null, indicator_offset: null, indicator_width: null },
+ member_allocations: [{}]
+ };
+
+ if (result.rows.length) {
+
+ const mergedAllocation = await this.mergeAllocations(result.rows);
+
+ result.rows = mergedAllocation;
+
+ for (const allocation of result.rows) {
+
+ allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
+ allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
+
+ const styles = this.setAllocationIndicator(allocation);
+
+ allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
+ allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
+
+ }
+
+ body.member_allocations = result.rows;
+
+ }
+ const qP = `SELECT id,
+ allocated_from,
+ allocated_to,
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1
+ UNION
+ SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1) AS dates) rec) AS date_union
+ FROM project_member_allocations
+ WHERE project_id = $1`;
+
+ const resultP = await db.query(qP, [projectId]);
+
+ if (resultP.rows.length) {
+ const project = resultP.rows[0];
+
+ const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
+
+ if (lowestDate) project.start_date = lowestDate;
+ if (highestDate) project.end_date = highestDate;
+
+ project.start_date = project.start_date ? moment(project.start_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+ project.end_date = project.end_date ? moment(project.end_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+
+ const styles = this.setIndicatorWithLogIndicator(project);
+
+ project.indicator_offset = styles.indicatorOffset;
+ project.indicator_width = styles.indicatorWidth;
+
+ body.project_allocation = project;
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, body));
+ }
+
+ @HandleExceptions()
+ public static async getSingleProjectMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+
+ const projectId = req.params.id as string;
+ const teamMemberId = req.query.team_member_id as string;
+ const timeZone = req.query.timeZone as string;
+ const projectIndicatorRefresh = req.query.isProjectRefresh;
+
+ const q = `SELECT id,
+ allocated_from,
+ allocated_to,
+ (SELECT COALESCE(ROW_TO_JSON(rec), '{}'::JSON)
+ FROM (SELECT MIN(min_date) AS start_date, MAX(max_date) AS end_date
+ FROM (SELECT MIN(allocated_from) AS min_date, MAX(allocated_from) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1
+ UNION
+ SELECT MIN(allocated_to) AS min_date, MAX(allocated_to) AS max_date
+ FROM project_member_allocations
+ WHERE project_id = $1) AS dates) rec) AS date_union
+ FROM project_member_allocations
+ WHERE team_member_id = $2
+ AND project_id = $1`;
+
+ const result = await db.query(q, [projectId, teamMemberId]);
+
+ const body = {
+ project_allocation: { start_date: null, end_date: null, indicator_offset: null, indicator_width: null },
+ member_allocations: [{}]
+ };
+
+ if (result.rows.length) {
+ const project = result.rows[0];
+
+ const { lowestDate, highestDate } = await this.setIndicatorDates(project, timeZone);
+
+ if (lowestDate) project.start_date = lowestDate;
+ if (highestDate) project.end_date = highestDate;
+
+ project.start_date = project.start_date ? moment(project.start_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+ project.end_date = project.end_date ? moment(project.end_date).format("YYYY-MM-DD") : moment().format("YYYY-MM-DD");
+
+ const styles = this.setIndicatorWithLogIndicator(project);
+
+ project.indicator_offset = styles.indicatorOffset;
+ project.indicator_width = styles.indicatorWidth;
+
+ const mergedAllocation = await this.mergeAllocations(result.rows);
+
+ result.rows = mergedAllocation;
+
+ for (const allocation of result.rows) {
+
+ allocation.allocated_from = allocation.allocated_from ? momentTime.tz(allocation.allocated_from, `${timeZone}`).format("YYYY-MM-DD") : null;
+ allocation.allocated_to = allocation.allocated_to ? momentTime.tz(allocation.allocated_to, `${timeZone}`).format("YYYY-MM-DD") : null;
+
+ const styles = this.setAllocationIndicator(allocation);
+
+ allocation.indicator_offset = styles?.indicatorOffset ? styles?.indicatorOffset : 0;
+ allocation.indicator_width = styles?.indicatorWidth ? styles?.indicatorWidth : 0;
+
+ }
+
+ body.member_allocations = result.rows;
+ body.project_allocation = project;
+
+ }
+
+ return res.status(200).send(new ServerResponse(true, body));
+
+ }
+
+ private static async mergeAllocations(allocations: { id: string | null, allocated_from: string | null, allocated_to: string | null, indicator_offset: number, indicator_width: number }[]) {
+
+ if (!allocations.length) return [];
+
+ allocations.sort((a, b) => moment(a.allocated_from).diff(moment(b.allocated_from)));
+
+ const mergedRanges = [];
+
+ let currentRange = { ...allocations[0], ids: [allocations[0].id] };
+
+ for (let i = 1; i < allocations.length; i++) {
+ const nextRange = allocations[i];
+
+ if (moment(currentRange.allocated_to).isSameOrAfter(nextRange.allocated_from)) {
+ currentRange.allocated_to = moment.max(moment(currentRange.allocated_to), moment(nextRange.allocated_to)).toISOString();
+ currentRange.ids.push(nextRange.id);
+ } else {
+ mergedRanges.push({ ...currentRange });
+ currentRange = { ...nextRange, ids: [nextRange.id] };
+ }
+ }
+
+ mergedRanges.push({ ...currentRange });
+
+ return mergedRanges;
+
+ }
+
+
+ private static async setIndicatorDates(item: any, timeZone: string) {
+ const datesToCheck = [];
+
+ item.date_union.start_date = item.date_union.start_date ? momentTime.tz(item.date_union.start_date, `${timeZone}`).format("YYYY-MM-DD") : null;
+ item.date_union.end_date = item.date_union.end_date ? momentTime.tz(item.date_union.end_date, `${timeZone}`).format("YYYY-MM-DD") : null;
+
+ for (const dateKey in item) {
+ if (item[dateKey as keyof IDateUnions] && item[dateKey as keyof IDateUnions].start_date) {
+ datesToCheck.push(moment(item[dateKey as keyof IDateUnions].start_date));
+ }
+ if (item[dateKey as keyof IDateUnions] && item[dateKey as keyof IDateUnions].end_date) {
+ datesToCheck.push(moment(item[dateKey as keyof IDateUnions].end_date));
+ }
+ }
+
+ const validDateToCheck = datesToCheck.filter((date) => date.isValid());
+
+ const lowestDate = validDateToCheck.length ? moment.min(validDateToCheck).format("YYYY-MM-DD") : null;
+ const highestDate = validDateToCheck.length ? moment.max(validDateToCheck).format("YYYY-MM-DD") : null;
+
+
+ return {
+ lowestDate,
+ highestDate
+ };
+
+ }
+
+ @HandleExceptions()
+ public static async deleteMemberAllocations(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const ids = req.body.toString() as string;
+ const q = `DELETE FROM project_member_allocations WHERE id IN (${(ids || "").split(",").map(s => `'${s}'`).join(",")})`;
+ await db.query(q);
+ return res.status(200).send(new ServerResponse(true, []));
+ }
+
+ // ********************************************
+
+ private static isCountsOnly(query: ParsedQs) {
+ return query.count === "true";
+ }
+
+ public static isTasksOnlyReq(query: ParsedQs) {
+ return ScheduleControllerV2.isCountsOnly(query) || query.parent_task;
+ }
+
+ private static flatString(text: string) {
+ return (text || "").split(" ").map(s => `'${s}'`).join(",");
+ }
+
+ private static getFilterByMembersWhereClosure(text: string) {
+ return text
+ ? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id IN (${this.flatString(text)}))`
+ : "";
+ }
+
+ private static getStatusesQuery(filterBy: string) {
+ return filterBy === "member"
+ ? `, (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code
+ FROM task_statuses
+ INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
+ WHERE project_id = t.project_id
+ ORDER BY task_statuses.name) rec) AS statuses`
+ : "";
+ }
+
+ public static async getTaskCompleteRatio(taskId: string): Promise<{
+ ratio: number;
+ total_completed: number;
+ total_tasks: number;
+ } | null> {
+ try {
+ const result = await db.query("SELECT get_task_complete_ratio($1) AS info;", [taskId]);
+ const [data] = result.rows;
+ data.info.ratio = +data.info.ratio.toFixed();
+ return data.info;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private static getQuery(userId: string, options: ParsedQs) {
+ const searchField = options.search ? "t.name" : "sort_order";
+ const { searchQuery, sortField } = ScheduleControllerV2.toPaginationOptions(options, searchField);
+
+ const isSubTasks = !!options.parent_task;
+
+ const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order";
+ const membersFilter = ScheduleControllerV2.getFilterByMembersWhereClosure(options.members as string);
+ const statusesQuery = ScheduleControllerV2.getStatusesQuery(options.filterBy as string);
+
+ const archivedFilter = options.archived === "true" ? "archived IS TRUE" : "archived IS FALSE";
+
+
+ let subTasksFilter;
+
+ if (options.isSubtasksInclude === "true") {
+ subTasksFilter = "";
+ } else {
+ subTasksFilter = isSubTasks ? "parent_task_id = $2" : "parent_task_id IS NULL";
+ }
+
+ const filters = [
+ subTasksFilter,
+ (isSubTasks ? "1 = 1" : archivedFilter),
+ membersFilter
+ ].filter(i => !!i).join(" AND ");
+
+ return `
+ SELECT id,
+ name,
+ t.project_id AS project_id,
+ t.parent_task_id,
+ t.parent_task_id IS NOT NULL AS is_sub_task,
+ (SELECT name FROM tasks WHERE id = t.parent_task_id) AS parent_task_name,
+ (SELECT COUNT(*)
+ FROM tasks
+ WHERE parent_task_id = t.id)::INT AS sub_tasks_count,
+
+ t.status_id AS status,
+ t.archived,
+ t.sort_order,
+
+ (SELECT phase_id FROM task_phase WHERE task_id = t.id) AS phase_id,
+ (SELECT name
+ FROM project_phases
+ WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_name,
+
+
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) AS status_color,
+
+ (SELECT COALESCE(ROW_TO_JSON(r), '{}'::JSON)
+ FROM (SELECT is_done, is_doing, is_todo
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = t.status_id)) r) AS status_category,
+
+ (CASE
+ WHEN EXISTS(SELECT 1
+ FROM tasks_with_status_view
+ WHERE tasks_with_status_view.task_id = t.id
+ AND is_done IS TRUE) THEN 1
+ ELSE 0 END) AS parent_task_completed,
+ (SELECT get_task_assignees(t.id)) AS assignees,
+ (SELECT COUNT(*)
+ FROM tasks_with_status_view tt
+ WHERE tt.parent_task_id = t.id
+ AND tt.is_done IS TRUE)::INT
+ AS completed_sub_tasks,
+
+ (SELECT id FROM task_priorities WHERE id = t.priority_id) AS priority,
+ (SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
+ total_minutes,
+ start_date,
+ end_date ${statusesQuery}
+ FROM tasks t
+ WHERE ${filters} ${searchQuery} AND project_id = $1
+ ORDER BY end_date DESC NULLS LAST
+ `;
+ }
+
+ public static async getGroups(groupBy: string, projectId: string): Promise {
+ let q = "";
+ let params: any[] = [];
+ switch (groupBy) {
+ case GroupBy.STATUS:
+ q = `
+ SELECT id,
+ name,
+ (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id),
+ category_id
+ FROM task_statuses
+ WHERE project_id = $1
+ ORDER BY sort_order;
+ `;
+ params = [projectId];
+ break;
+ case GroupBy.PRIORITY:
+ q = `SELECT id, name, color_code
+ FROM task_priorities
+ ORDER BY value DESC;`;
+ break;
+ case GroupBy.LABELS:
+ q = `
+ SELECT id, name, color_code
+ FROM team_labels
+ WHERE team_id = $2
+ AND EXISTS(SELECT 1
+ FROM tasks
+ WHERE project_id = $1
+ AND EXISTS(SELECT 1 FROM task_labels WHERE task_id = tasks.id AND label_id = team_labels.id))
+ ORDER BY name;
+ `;
+ break;
+ case GroupBy.PHASE:
+ q = `
+ SELECT id, name, color_code, start_date, end_date
+ FROM project_phases
+ WHERE project_id = $1
+ ORDER BY name;
+ `;
+ params = [projectId];
+ break;
+
+ default:
+ break;
+ }
+
+ const result = await db.query(q, params);
+ for (const row of result.rows) {
+ row.isExpand = true;
+ }
+ return result.rows;
+ }
+
+ @HandleExceptions()
+ public static async getList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const groupBy = (req.query.group || GroupBy.STATUS) as string;
+
+ const q = ScheduleControllerV2.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+
+ const result = await db.query(q, params);
+ const tasks = [...result.rows];
+
+ const groups = await this.getGroups(groupBy, req.params.id);
+ const map = groups.reduce((g: { [x: string]: IScheduleTaskGroup }, group) => {
+ if (group.id)
+ g[group.id] = new IScheduleTaskListGroup(group);
+ return g;
+ }, {});
+
+ this.updateMapByGroup(tasks, groupBy, map);
+
+ const updatedGroups = Object.keys(map).map(key => {
+ const group = map[key];
+
+ if (groupBy === GroupBy.PHASE)
+ group.color_code = getColor(group.name) + TASK_PRIORITY_COLOR_ALPHA;
+
+ return {
+ id: key,
+ ...group
+ };
+ });
+
+ return res.status(200).send(new ServerResponse(true, updatedGroups));
+ }
+
+ public static updateMapByGroup(tasks: any[], groupBy: string, map: { [p: string]: IScheduleTaskGroup }) {
+ let index = 0;
+ const unmapped = [];
+ for (const task of tasks) {
+ task.index = index++;
+ ScheduleControllerV2.updateTaskViewModel(task);
+ if (groupBy === GroupBy.STATUS) {
+ map[task.status]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PRIORITY) {
+ map[task.priority]?.tasks.push(task);
+ } else if (groupBy === GroupBy.PHASE && task.phase_id) {
+ map[task.phase_id]?.tasks.push(task);
+ } else {
+ unmapped.push(task);
+ }
+ }
+
+ if (unmapped.length) {
+ map[UNMAPPED] = {
+ name: UNMAPPED,
+ category_id: null,
+ color_code: "#f0f0f0",
+ tasks: unmapped,
+ isExpand: true
+ };
+ }
+ }
+
+
+ @HandleExceptions()
+ public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const isSubTasks = !!req.query.parent_task;
+ const q = ScheduleControllerV2.getQuery(req.user?.id as string, req.query);
+ const params = isSubTasks ? [req.params.id || null, req.query.parent_task] : [req.params.id || null];
+ const result = await db.query(q, params);
+
+ let data: any[] = [];
+
+ // if true, we only return the record count
+ if (this.isCountsOnly(req.query)) {
+ [data] = result.rows;
+ } else { // else we return a flat list of tasks
+ data = [...result.rows];
+ for (const task of data) {
+ ScheduleControllerV2.updateTaskViewModel(task);
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+
+}
diff --git a/worklenz-backend/src/controllers/shared-projects-controller.ts b/worklenz-backend/src/controllers/shared-projects-controller.ts
new file mode 100644
index 00000000..70dc17ff
--- /dev/null
+++ b/worklenz-backend/src/controllers/shared-projects-controller.ts
@@ -0,0 +1,61 @@
+import {createHmac} from "crypto";
+
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class SharedprojectsController extends WorklenzControllerBase {
+ private static getShareLink(hash: string) {
+ return `https://${process.env.HOSTNAME}/share/${hash}`;
+ }
+
+ private static createShareInfo(name?: string, createdAt?: string, hash?: string) {
+ if (!name || !createdAt || !hash) return null;
+ return {
+ url: this.getShareLink(hash),
+ created_by: name?.split(" ")[0],
+ created_at: createdAt
+ };
+ }
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ INSERT INTO shared_projects (project_id, team_id, enabled_by, hash_value)
+ VALUES ($1, $2, $3, $4)
+ RETURNING id, created_at;
+ `;
+
+ const hash = createHmac("sha256", req.body.project_id).digest("hex");
+
+ const result = await db.query(q, [req.body.project_id, req.user?.team_id, req.user?.id, hash]);
+ const [data] = result.rows;
+ if (!data?.id)
+ return res.status(400).send(new ServerResponse(true, null));
+ return res.status(200).send(new ServerResponse(true, this.createShareInfo(req.user?.name?.split(" ")[0], data.createdAt, hash)));
+ }
+
+ @HandleExceptions()
+ public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT hash_value, created_at, (SELECT name FROM users WHERE id = enabled_by)
+ FROM shared_projects
+ WHERE project_id = $1
+ AND team_id = $2;
+ `;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+ const [data] = result.rows;
+ return res.status(200).send(new ServerResponse(true, this.createShareInfo(data?.name, data?.created_at, data?.hash_value)));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE FROM shared_projects WHERE project_id = $1 AND team_id = $2;`;
+ const result = await db.query(q, [req.params.id, req.user?.team_id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/sub-tasks-controller.ts b/worklenz-backend/src/controllers/sub-tasks-controller.ts
new file mode 100644
index 00000000..279119e6
--- /dev/null
+++ b/worklenz-backend/src/controllers/sub-tasks-controller.ts
@@ -0,0 +1,119 @@
+import moment from "moment";
+
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
+import {getColor} from "../shared/utils";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+
+export default class SubTasksController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getNames(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `SELECT name FROM tasks WHERE archived IS FALSE AND parent_task_id = $1;`;
+ const result = await db.query(q, [req.params.id]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT t.id,
+ t.name,
+ t.description,
+ t.project_id,
+ t.parent_task_id,
+ t.priority_id AS priority,
+ tp.name AS priority_name,
+ t.end_date,
+ (ts.id) AS status,
+ (ts.name) AS status_name,
+ TRUE AS is_sub_task,
+ (tsc.color_code) AS status_color,
+ (SELECT name FROM projects WHERE id = t.project_id) AS project_name,
+ (SELECT value FROM task_priorities WHERE id = t.priority_id) AS priority_value,
+ total_minutes,
+ (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent,
+ (SELECT get_task_assignees(t.id)) AS assignees,
+ (SELECT ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(r)))
+ FROM (SELECT task_labels.label_id AS id,
+ (SELECT name FROM team_labels WHERE id = task_labels.label_id),
+ (SELECT color_code FROM team_labels WHERE id = task_labels.label_id)
+ FROM task_labels
+ WHERE task_id = t.id
+ ORDER BY name) r) AS labels,
+ (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON)
+ FROM (SELECT task_statuses.id, task_statuses.name, stsc.color_code
+ FROM task_statuses
+ INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
+ WHERE project_id = t.project_id
+ ORDER BY task_statuses.name) rec) AS statuses
+ FROM tasks t
+ INNER JOIN task_statuses ts ON ts.id = t.status_id
+ INNER JOIN task_priorities tp ON tp.id = t.priority_id
+ LEFT JOIN sys_task_status_categories tsc ON ts.category_id = tsc.id
+ WHERE parent_task_id = $1
+ ORDER BY created_at;
+ `;
+ const result = await db.query(q, [req.params.id]);
+
+ for (const task of result.rows) {
+ task.priority_color = PriorityColorCodes[task.priority_value] || null;
+
+ task.time_spent = {hours: Math.floor(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60};
+ task.time_spent_string = `${task.time_spent.hours}h ${task.time_spent.minutes}m`;
+ task.total_time_string = `${Math.floor(task.total_minutes / 60)}h ${task.total_minutes % 60}m`;
+
+ task.assignees.map((a: any) => a.color_code = getColor(a.name));
+ task.names = this.createTagList(task.assignees);
+ task.labels = this.createTagList(task.labels, 2);
+
+ task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
+ task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA;
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async getSubTasksRoadMap(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const dates = req.body;
+ const q = `
+ SELECT tasks.id,
+ tasks.name,
+ tasks.start_date,
+ tasks.end_date,
+ tp.name AS priority,
+ tasks.end_date,
+ (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
+ (SELECT color_code
+ FROM sys_task_status_categories
+ WHERE id = (SELECT category_id FROM task_statuses WHERE id = tasks.status_id)) AS status_color,
+ (SELECT get_task_assignees(tasks.id)) AS assignees
+ FROM tasks
+ INNER JOIN task_statuses ts ON ts.task_id = tasks.id
+ INNER JOIN task_priorities tp ON tp.id = tasks.priority_id
+ WHERE archived IS FALSE AND parent_task_id = $1
+ ORDER BY created_at DESC;
+ `;
+ const result = await db.query(q, [req.params.id]);
+
+ const maxInlineNames = 4;
+ for (const task of result.rows) {
+ task.assignees.map((a: any) => a.color_code = getColor(a.name));
+ task.names = this.createTagList(task.assignees);
+
+ if (task?.assignees.length <= maxInlineNames) {
+ const min: number = dates.findIndex((date: any) => moment(task.start_date).isSame(date.date, "days"));
+ const max: number = dates.findIndex((date: any) => moment(task.end_date).isSame(date.date, "days"));
+ task.min = min + 1;
+ task.max = max > 0 ? max + 2 : max;
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/task-comments-controller.ts b/worklenz-backend/src/controllers/task-comments-controller.ts
new file mode 100644
index 00000000..a4bc17b0
--- /dev/null
+++ b/worklenz-backend/src/controllers/task-comments-controller.ts
@@ -0,0 +1,265 @@
+import { IWorkLenzRequest } from "../interfaces/worklenz-request";
+import { IWorkLenzResponse } from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import { ServerResponse } from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import { NotificationsService } from "../services/notifications/notifications.service";
+import { log_error } from "../shared/utils";
+import { HTML_TAG_REGEXP } from "../shared/constants";
+import { getBaseUrl } from "../cron_jobs/helpers";
+import { ICommentEmailNotification } from "../interfaces/comment-email-notification";
+import { sendTaskComment } from "../shared/email-notifications";
+
+interface ITaskAssignee {
+ team_member_id: string;
+ project_member_id: string;
+ name: string;
+ email_notifications_enabled: string;
+ avatar_url: string;
+ user_id: string;
+ email: string;
+ socket_id: string;
+ team_id: string;
+ user_name: string;
+}
+
+interface IMailConfig {
+ message: string;
+ receiverEmail: string;
+ receiverName: string;
+ content: string;
+ commentId: string;
+ projectId: string;
+ taskId: string;
+ teamName: string;
+ projectName: string;
+ taskName: string;
+}
+
+interface IMention {
+ team_member_id: string;
+ name: string;
+}
+
+async function getAssignees(taskId: string): Promise> {
+ const result1 = await db.query("SELECT get_task_assignees($1) AS assignees;", [taskId]);
+ const [d] = result1.rows;
+ return d.assignees || [];
+}
+
+export default class TaskCommentsController extends WorklenzControllerBase {
+
+ private static replaceContent(messageContent: string, mentions: IMention[]) {
+ const mentionNames = mentions.map(mention => mention.name);
+
+ const replacedContent = mentionNames.reduce(
+ (content, mentionName, index) => {
+ const regex = new RegExp(`@${mentionName}`, "g");
+ return content.replace(regex, `{${index}}`);
+ },
+ messageContent
+ );
+
+ return replacedContent;
+ }
+
+ private static async getUserDataByTeamMemberId(senderUserId: string, teamMemberId: string, projectId: string) {
+ const q = `
+ SELECT id,
+ socket_id,
+ users.name AS user_name,
+ (SELECT email_notifications_enabled
+ FROM notification_settings
+ WHERE notification_settings.team_id = (SELECT team_id FROM team_members WHERE id = $2)
+ AND notification_settings.user_id = users.id),
+ (SELECT name FROM teams WHERE id = (SELECT team_id FROM team_members WHERE id = $2)) AS team,
+ (SELECT name FROM projects WHERE id = $3) AS project,
+ (SELECT color_code FROM projects WHERE id = $3) AS project_color
+ FROM users
+ WHERE id != $1
+ AND id IN (SELECT user_id FROM team_members WHERE id = $2);
+ `;
+ const result = await db.query(q, [senderUserId, teamMemberId, projectId]);
+ const [data] = result.rows;
+ return data;
+ }
+
+ private static async updateComment(commentId: string, messageId: string) {
+ if (!commentId || messageId) return;
+ try {
+ await db.query("UPDATE task_comments SET ses_message_id = $2 WHERE id = $1;", [commentId, messageId]);
+ } catch (e) {
+ log_error(e);
+ }
+ }
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ req.body.user_id = req.user?.id;
+ req.body.team_id = req.user?.team_id;
+ const {mentions} = req.body;
+
+ let commentContent = req.body.content;
+ if (mentions.length > 0) {
+ commentContent = await this.replaceContent(commentContent, mentions);
+ }
+
+ req.body.content = commentContent;
+
+ const q = `SELECT create_task_comment($1) AS comment;`;
+ const result = await db.query(q, [JSON.stringify(req.body)]);
+ const [data] = result.rows;
+
+ const response = data.comment;
+
+ const mentionMessage = `${req.user?.name} has mentioned you in a comment on ${response.task_name} (${response.team_name})`;
+ // const mentions = [...new Set(req.body.mentions || [])] as string[]; // remove duplicates
+
+ const assignees = await getAssignees(req.body.task_id);
+
+ const commentMessage = `${req.user?.name} added a comment on ${response.task_name} (${response.team_name})`;
+ for (const member of assignees || []) {
+ if (member.user_id && member.user_id === req.user?.id) continue;
+
+ void NotificationsService.createNotification({
+ userId: member.user_id,
+ teamId: req.user?.team_id as string,
+ socketId: member.socket_id,
+ message: commentMessage,
+ taskId: req.body.task_id,
+ projectId: response.project_id
+ });
+
+ if (member.email_notifications_enabled)
+ await this.sendMail({
+ message: commentMessage,
+ receiverEmail: member.email,
+ receiverName: member.name,
+ content: req.body.content,
+ commentId: response.id,
+ projectId: response.project_id,
+ taskId: req.body.task_id,
+ teamName: response.team_name,
+ projectName: response.project_name,
+ taskName: response.task_name
+ });
+ }
+
+ const senderUserId = req.user?.id as string;
+
+ for (const mention of mentions) {
+ if (mention) {
+ const member = await this.getUserDataByTeamMemberId(senderUserId, mention.team_member_id, response.project_id);
+ if (member) {
+
+ NotificationsService.sendNotification({
+ team: member.team,
+ receiver_socket_id: member.socket_id,
+ message: mentionMessage,
+ task_id: req.body.task_id,
+ project_id: response.project_id,
+ project: member.project,
+ project_color: member.project_color,
+ team_id: req.user?.team_id as string
+ });
+
+ if (member.email_notifications_enabled)
+ await this.sendMail({
+ message: mentionMessage,
+ receiverEmail: member.email,
+ receiverName: member.user_name,
+ content: req.body.content,
+ commentId: response.id,
+ projectId: response.project_id,
+ taskId: req.body.task_id,
+ teamName: response.team_name,
+ projectName: response.project_name,
+ taskName: response.task_name
+ });
+ }
+
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, data.comment));
+ }
+
+ private static async sendMail(config: IMailConfig) {
+ const subject = config.message.replace(HTML_TAG_REGEXP, "");
+ const taskUrl = `${getBaseUrl()}/worklenz/projects/${config.projectId}?tab=tasks-list&task=${config.taskId}&focus=comments`;
+ const settingsUrl = `${getBaseUrl()}/worklenz/settings/notifications`;
+
+ const data: ICommentEmailNotification = {
+ greeting: `Hi ${config.receiverName}`,
+ summary: subject,
+ team: config.teamName,
+ project_name: config.projectName,
+ comment: config.content,
+ task: config.taskName,
+ settings_url: settingsUrl,
+ task_url: taskUrl,
+ };
+
+ const messageId = await sendTaskComment(config.receiverEmail, data);
+ if (messageId) {
+ void TaskCommentsController.updateComment(config.commentId, messageId);
+ }
+ }
+
+ @HandleExceptions()
+ public static async getByTaskId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT task_comments.id,
+ tc.text_content AS content,
+ task_comments.user_id,
+ task_comments.team_member_id,
+ (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS member_name,
+ u.avatar_url,
+ task_comments.created_at,
+ (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
+ FROM (SELECT tmiv.name AS user_name,
+ tmiv.email AS user_email
+ FROM task_comment_mentions tcm
+ LEFT JOIN team_member_info_view tmiv ON tcm.informed_by = tmiv.team_member_id
+ WHERE tcm.comment_id = task_comments.id) rec) AS mentions
+ FROM task_comments
+ INNER JOIN task_comment_contents tc ON task_comments.id = tc.comment_id
+ INNER JOIN team_members tm ON task_comments.team_member_id = tm.id
+ LEFT JOIN users u ON tm.user_id = u.id
+ WHERE task_comments.task_id = $1
+ ORDER BY task_comments.created_at DESC;
+ `;
+ const result = await db.query(q, [req.params.id]); // task id
+
+ for (const comment of result.rows) {
+ comment.content = await comment.content.replace(/\n/g, "");
+ const {mentions} = comment;
+ if (mentions.length > 0) {
+ const placeHolders = comment.content.match(/{\d+}/g);
+ if (placeHolders) {
+ placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
+ const index = parseInt(placeHolder.match(/\d+/)[0]);
+ if (index >= 0 && index < comment.mentions.length) {
+ comment.content = comment.content.replace(placeHolder, ` @${comment.mentions[index].user_name} `);
+ }
+ });
+ }
+ }
+ }
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `DELETE
+ FROM task_comments
+ WHERE id = $1
+ AND task_id = $2
+ AND user_id = $3;`;
+ const result = await db.query(q, [req.params.id, req.params.taskId, req.user?.id || null]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/task-list-columns-controller.ts b/worklenz-backend/src/controllers/task-list-columns-controller.ts
new file mode 100644
index 00000000..0903abf7
--- /dev/null
+++ b/worklenz-backend/src/controllers/task-list-columns-controller.ts
@@ -0,0 +1,39 @@
+import db from "../config/db";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+
+export default class TaskListColumnsController extends WorklenzControllerBase {
+ @HandleExceptions()
+ public static async getProjectTaskListColumns(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT name,
+ key,
+ index,
+ pinned,
+ (SELECT phase_label FROM projects WHERE id = $1) AS phase_label
+ FROM project_task_list_cols
+ WHERE project_id = $1
+ ORDER BY index;
+ `;
+
+ const result = await db.query(q, [req.params.id]);
+ const phase = result.rows.find(phase => phase.key === "PHASE");
+ if (phase)
+ phase.name = phase.phase_label;
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async toggleColumn(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `UPDATE project_task_list_cols
+ SET pinned = $3
+ WHERE project_id = $1
+ AND key = $2;`;
+ const result = await db.query(q, [req.params.id, req.body.key, !!req.body.pinned]);
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+}
diff --git a/worklenz-backend/src/controllers/task-phases-controller.ts b/worklenz-backend/src/controllers/task-phases-controller.ts
new file mode 100644
index 00000000..e72fbbab
--- /dev/null
+++ b/worklenz-backend/src/controllers/task-phases-controller.ts
@@ -0,0 +1,119 @@
+import {IWorkLenzRequest} from "../interfaces/worklenz-request";
+import {IWorkLenzResponse} from "../interfaces/worklenz-response";
+
+import db from "../config/db";
+import {ServerResponse} from "../models/server-response";
+import WorklenzControllerBase from "./worklenz-controller-base";
+import HandleExceptions from "../decorators/handle-exceptions";
+import {getColor} from "../shared/utils";
+import {TASK_STATUS_COLOR_ALPHA} from "../shared/constants";
+
+export default class TaskPhasesController extends WorklenzControllerBase {
+ private static readonly DEFAULT_PHASE_COLOR = "#fbc84c";
+
+ @HandleExceptions()
+ public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ if (!req.query.id)
+ return res.status(400).send(new ServerResponse(false, null, "Invalid request"));
+
+ const q = `
+ INSERT INTO project_phases (name, color_code, project_id, sort_index)
+ VALUES (
+ CONCAT('Untitled Phase (', (SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1, ')'),
+ $1,
+ $2,
+ (SELECT COUNT(*) FROM project_phases WHERE project_id = $2) + 1)
+ RETURNING id, name, color_code, sort_index;
+ `;
+
+ req.body.color_code = this.DEFAULT_PHASE_COLOR;
+
+ const result = await db.query(q, [req.body.color_code, req.query.id]);
+ const [data] = result.rows;
+
+ data.color_code = getColor(data.name) + TASK_STATUS_COLOR_ALPHA;
+
+ return res.status(200).send(new ServerResponse(true, data));
+ }
+
+ @HandleExceptions()
+ public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise {
+ const q = `
+ SELECT id, name, color_code, (SELECT COUNT(*) FROM task_phase WHERE phase_id = project_phases.id) AS usage
+ FROM project_phases
+ WHERE project_id = $1
+ ORDER BY sort_index DESC;
+ `;
+ const result = await db.query(q, [req.query.id]);
+
+ for (const phase of result.rows)
+ phase.color_code = phase.color_code + TASK_STATUS_COLOR_ALPHA;
+
+ return res.status(200).send(new ServerResponse(true, result.rows));
+ }
+
+ @HandleExceptions()
+ public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise