From 298ca6beeb2679800bad0912a4ef8af7bfaa55c3 Mon Sep 17 00:00:00 2001 From: chamikaJ Date: Fri, 17 May 2024 09:32:30 +0530 Subject: [PATCH] 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 { + const q = ` + UPDATE project_phases + SET name = $3 + WHERE id = $1 + AND project_id = $2 + RETURNING id, name, color_code; + `; + + const result = await db.query(q, [req.params.id, req.query.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 project_phases SET color_code = $3 WHERE id = $1 AND project_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 updateLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + UPDATE projects + 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 updateSortOrder (req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const body = { + phases: req.body.phases.reverse(), + project_id: req.body.project_id + }; + + const q = `SELECT handle_phase_sort_order($1);`; + const result = await db.query(q, [JSON.stringify(body)]); + 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_phases + WHERE id = $1 + AND project_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/task-priorities-controller.ts b/worklenz-backend/src/controllers/task-priorities-controller.ts new file mode 100644 index 00000000..78d4b666 --- /dev/null +++ b/worklenz-backend/src/controllers/task-priorities-controller.ts @@ -0,0 +1,28 @@ +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 {PriorityColorCodes} from "../shared/constants"; + +export default class TaskPrioritiesController extends WorklenzControllerBase { + @HandleExceptions() + public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + 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"]; + 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 From priorities 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)); + } + +} diff --git a/worklenz-backend/src/controllers/task-statuses-controller.ts b/worklenz-backend/src/controllers/task-statuses-controller.ts new file mode 100644 index 00000000..8ef725ff --- /dev/null +++ b/worklenz-backend/src/controllers/task-statuses-controller.ts @@ -0,0 +1,161 @@ +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"; + +const existsErrorMessage = "At least one status should exists under each category."; + +export default class TaskStatusesController extends WorklenzControllerBase { + + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + INSERT INTO task_statuses (name, project_id, team_id, category_id, sort_order) + VALUES ($1, $2, $3, $4, (SELECT MAX(sort_order) FROM task_statuses WHERE project_id = $2) + 1); + `; + const result = await db.query(q, [req.body.name, req.body.project_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_task_status($1, $2)`; + const result = await db.query(q, [JSON.stringify(req.body), team_id]); + const data = result.rows[0].create_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.project) + return res.status(400).send(new ServerResponse(false, null)); + + const q = ` + SELECT task_statuses.id, + task_statuses.name, + stsc.color_code, + stsc.name AS category_name, + task_statuses.category_id, + stsc.description + 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 result = await db.query(q, [req.query.project, 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)); + } + + private static async getStatusByGroups(projectId: string) { + if (!projectId) return; + + const q = ``; + const result = await db.query(q, [projectId]); + return result.rows; + } + + @HandleExceptions() + public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + 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 task_statuses.id = $1 + AND project_id = $2; + `; + const result = await db.query(q, [req.params.id, req.query.project_id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + private static async hasMoreCategories(statusId: string, projectId: string) { + if (!statusId || !projectId) + return false; + + const q = ` + SELECT COUNT(*) AS count + FROM task_statuses + WHERE category_id = (SELECT category_id FROM task_statuses WHERE id = $1) + AND project_id = $2; + `; + + const result = await db.query(q, [statusId, projectId]); + const [data] = result.rows; + return +data.count >= 2; + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const hasMoreCategories = await TaskStatusesController.hasMoreCategories(req.params.id, req.body.project_id); + + if (!hasMoreCategories) + return res.status(200).send(new ServerResponse(false, null, existsErrorMessage).withTitle("Status update failed!")); + + const q = ` + UPDATE 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 project_id = $3 + RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id); + `; + const result = await db.query(q, [req.params.id, req.body.name, req.body.project_id, req.body.category_id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async updateName(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + UPDATE task_statuses + SET name = $2 + WHERE id = $1 + AND project_id = $3 + RETURNING (SELECT color_code FROM sys_task_status_categories WHERE id = task_statuses.category_id); + `; + const result = await db.query(q, [req.params.id, req.body.name, req.body.project_id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async updateStatusOrder(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT update_status_order($1);`; + const result = await db.query(q, [JSON.stringify(req.body.status_order)]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions({ + raisedExceptions: { + "ERROR_ONE_SHOULD_EXISTS": existsErrorMessage + } + }) + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT move_tasks_and_delete_status($1)`; + + const body = { + id: req.params.id, + project_id: req.query.project, + replacing_status: req.query.replace + }; + + const result = await db.query(q, [JSON.stringify(body)]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } +} diff --git a/worklenz-backend/src/controllers/task-templates-controller.ts b/worklenz-backend/src/controllers/task-templates-controller.ts new file mode 100644 index 00000000..71435074 --- /dev/null +++ b/worklenz-backend/src/controllers/task-templates-controller.ts @@ -0,0 +1,79 @@ +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 TasktemplatesController extends WorklenzControllerBase { + @HandleExceptions({ + raisedExceptions: { + "TASK_TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.` + } +}) + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {name, tasks} = req.body; + const q = `SELECT create_task_template($1, $2, $3);`; + const result = await db.query(q, [name.trim(), req.user?.team_id, JSON.stringify(tasks)]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data, "Task template created successfully")); + } + + @HandleExceptions() + public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT id, name, created_at FROM task_templates WHERE team_id = $1 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 getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {id} = req.params; + const q = `SELECT id, name, + ((SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]'::JSON) + FROM (SELECT task_templates_tasks.name AS name, + task_templates_tasks.total_minutes AS total_minutes + FROM task_templates_tasks + WHERE template_id = task_templates.id) rec)) AS tasks + FROM task_templates + WHERE id = $1 + ORDER BY name`; + const result = await db.query(q, [id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions({ + raisedExceptions: { + "TASK_TEMPLATE_EXISTS_ERROR": `A template with the name "{0}" already exists. Please choose a different name.` + } +}) + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {name, tasks} = req.body; + const {id} = req.params; + + const q = `SELECT update_task_template($1, $2, $3, $4);`; + const result = await db.query(q, [id, name, JSON.stringify(tasks), req.user?.team_id]); + return res.status(200).send(new ServerResponse(true, result.rows, "Template updated.")); + } + + @HandleExceptions() + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {id} = req.params; + + const q = `DELETE FROM task_templates WHERE id = $1;`; + const result = await db.query(q, [id]); + return res.status(200).send(new ServerResponse(true, result.rows, "Template deleted.")); + } + + @HandleExceptions() + public static async import(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {id} = req.params; + + const q = `SELECT import_tasks_from_template($1, $2, $3);`; + const result = await db.query(q, [id, req.user?.id, JSON.stringify(req.body)]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data, "Tasks imported successfully!")); + } +} diff --git a/worklenz-backend/src/controllers/task-work-log-controller.ts b/worklenz-backend/src/controllers/task-work-log-controller.ts new file mode 100644 index 00000000..ed47b4b4 --- /dev/null +++ b/worklenz-backend/src/controllers/task-work-log-controller.ts @@ -0,0 +1,237 @@ +import Excel from "exceljs"; +import moment from "moment"; + +import {IWorkLenzRequest} from "../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../interfaces/worklenz-response"; + +import db from "../config/db"; +import {formatDuration, getColor, log_error, toSeconds} from "../shared/utils"; +import {ServerResponse} from "../models/server-response"; +import WorklenzControllerBase from "./worklenz-controller-base"; +import HandleExceptions from "../decorators/handle-exceptions"; +import momentTime from "moment-timezone"; + +export default class TaskWorklogController extends WorklenzControllerBase { + + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {id, seconds_spent, description, created_at, formatted_start} = req.body; + const q = `INSERT INTO task_work_log (time_spent, description, task_id, user_id, created_at) + VALUES ($1, $2, $3, $4, $5);`; + const params = [seconds_spent, description, id, req.user?.id, formatted_start]; + const result = await db.query(q, params); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + private static async getTimeLogs(id: string, timeZone: string) { + if (!id) return []; + + const q = ` + WITH time_logs AS ( + -- + SELECT id, + description, + time_spent, + created_at, + user_id, + logged_by_timer, + (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 + FROM task_work_log + WHERE task_id = $1 + -- + ) + SELECT id, + time_spent, + description, + created_at, + user_id, + logged_by_timer, + created_at AS start_time, + (created_at + INTERVAL '1 second' * time_spent) AS end_time, + user_name, + user_email, + avatar_url + FROM time_logs + ORDER BY created_at DESC; + `; + const result = await db.query(q, [id]); + if (timeZone) { + for (const res of result.rows) { + res.start_time = momentTime.tz(res.start_time, `${timeZone}`).format(); + res.end_time = momentTime.tz(res.end_time, `${timeZone}`).format(); + } + } + return result.rows; + } + + @HandleExceptions() + public static async getByTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const results = await this.getTimeLogs(req.params.id, req.query.time_zone_name as string); + + for (const item of results) + item.avatar_color = getColor(item.user_name); + + return res.status(200).send(new ServerResponse(true, results)); + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {seconds_spent, description, created_at, formatted_start} = req.body; + const q = ` + UPDATE task_work_log + SET time_spent = $3, + description = $4, + created_at = $5 + WHERE id = $1 + AND user_id = $2; + `; + const params = [req.params.id, req.user?.id, seconds_spent, description || null, formatted_start]; + const result = await db.query(q, params); + 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 task_work_log + WHERE id = $1 + AND task_id = $2 + AND user_id = $3;`; + const result = await db.query(q, [req.params.id, req.query.task, req.user?.id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + private static async getExportMetadata(id: string) { + const q = `SELECT name, (SELECT name FROM projects WHERE id = tasks.project_id) AS project_name + FROM tasks + WHERE id = $1;`; + const result = await db.query(q, [id]); + return result.rows[0] || null; + } + + private static async getUserTimeZone(id: string) { + if (id) { + const q = `SELECT utc_offset + FROM timezones + WHERE id = (SELECT timezone_id FROM users WHERE id = $1);`; + const result = await db.query(q, [id]); + const [data] = result.rows; + return data.utc_offset || null; + } + } + + @HandleExceptions() + public static async exportLog(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const results = await this.getTimeLogs(req.params.id, req.query.timeZone as string); + const metadata = await this.getExportMetadata(req.params.id); + const timezone = await this.getUserTimeZone(req.user?.id || ""); + + const exportDate = moment().format("MMM-DD-YYYY"); + const fileName = `${exportDate} - Task Timelog`; + const title = metadata.name.replace(/[\*\?\:\/\\\[\]]/g, "-"); + + const workbook = new Excel.Workbook(); + const sheet = workbook.addWorksheet(title); + + sheet.headerFooter = { + firstHeader: title + }; + + sheet.columns = [ + {header: "Reporter Name", key: "user_name", width: 25}, + {header: "Reporter Email", key: "user_email", width: 25}, + {header: "Start Time", key: "start_time", width: 25}, + {header: "End Time", key: "end_time", width: 25}, + {header: "Date", key: "created_at", width: 25}, + {header: "Work Description", key: "description", width: 25}, + {header: "Duration", key: "time_spent", width: 25}, + ]; + + sheet.getCell("A1").value = metadata.project_name; + sheet.mergeCells("A1:G1"); + sheet.getCell("A1").alignment = {horizontal: "center"}; + + sheet.getCell("A2").value = `${metadata.name} (${exportDate})`; + sheet.mergeCells("A2:G2"); + sheet.getCell("A2").alignment = {horizontal: "center"}; + + sheet.getRow(4).values = [ + "Reporter Name", + "Reporter Email", + "Start Time", + "End Time", + "Date", + "Work Description", + "Duration", + ]; + + const timeFormat = "MMM DD, YYYY h:mm:ss a"; + let totalLogged = 0; + + for (const item of results) { + totalLogged += parseFloat((item.time_spent || 0).toString()); + const data = { + user_name: item.user_name, + user_email: item.user_email, + start_time: moment(item.start_time).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat), + end_time: moment(item.end_time).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat), + created_at: moment(item.created_at).add(timezone.hours || 0, "hours").add(timezone.minutes || 0, "minutes").format(timeFormat), + description: item.description || "-", + time_spent: formatDuration(moment.duration(item.time_spent, "seconds")), + }; + sheet.addRow(data); + } + + sheet.getCell("A1").style.fill = { + type: "pattern", + pattern: "solid", + fgColor: {argb: "D9D9D9"} + }; + sheet.getCell("A1").font = { + size: 16 + }; + + sheet.getCell("A2").style.fill = { + type: "pattern", + pattern: "solid", + fgColor: {argb: "F2F2F2"} + }; + sheet.getCell("A2").font = { + size: 12 + }; + + sheet.getRow(4).font = { + bold: true + }; + + sheet.addRow({ + user_name: "", + user_email: "", + start_time: "Total", + end_time: "", + description: "", + created_at: "", + time_spent: formatDuration(moment.duration(totalLogged, "seconds")), + }); + + sheet.mergeCells(`A${sheet.rowCount}:F${sheet.rowCount}`); + + sheet.getCell(`A${sheet.rowCount}`).value = "Total"; + sheet.getCell(`A${sheet.rowCount}`).alignment = { + horizontal: "right" + }; + + 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/tasks-controller-base.ts b/worklenz-backend/src/controllers/tasks-controller-base.ts new file mode 100644 index 00000000..ae336b63 --- /dev/null +++ b/worklenz-backend/src/controllers/tasks-controller-base.ts @@ -0,0 +1,91 @@ +import WorklenzControllerBase from "./worklenz-controller-base"; +import {getColor} from "../shared/utils"; +import {PriorityColorCodes, TASK_PRIORITY_COLOR_ALPHA, TASK_STATUS_COLOR_ALPHA} from "../shared/constants"; +import moment from "moment/moment"; + +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 TasksControllerBase 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; + + task.time_spent = {hours: ~~(task.total_minutes_spent / 60), minutes: task.total_minutes_spent % 60}; + + task.comments_count = Number(task.comments_count) ? +task.comments_count : 0; + task.attachments_count = Number(task.attachments_count) ? +task.attachments_count : 0; + + if (typeof task.sub_tasks_count === "undefined") task.sub_tasks_count = "0"; + + task.is_sub_task = !!task.parent_task_id; + + task.time_spent_string = `${task.time_spent.hours}h ${(task.time_spent.minutes)}m`; + task.total_time_string = `${~~(task.total_minutes / 60)}h ${(task.total_minutes % 60)}m`; + + 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_color_code + ? task.phase_color_code : getColor(task.phase_name) + TASK_PRIORITY_COLOR_ALPHA; + } + + if (Array.isArray(task.assignees)) { + for (const assignee of task.assignees) { + assignee.color_code = getColor(assignee.name); + } + } + + task.names = TasksControllerBase.createTagList(task.assignees); + + task.all_labels = task.labels; + task.labels = TasksControllerBase.createTagList(task.labels, 2); + + task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA; + task.priority_color = task.priority_color + TASK_PRIORITY_COLOR_ALPHA; + + if (task.timer_start_time) + task.timer_start_time = moment(task.timer_start_time).valueOf(); + + const totalCompleted = +task.completed_sub_tasks + +task.parent_task_completed; + const totalTasks = +task.sub_tasks_count + 1; // +1 for parent + task.complete_ratio = TasksControllerBase.calculateTaskCompleteRatio(totalCompleted, totalTasks); + task.completed_count = totalCompleted; + task.total_tasks_count = totalTasks; + + task.width = 35; + + if (task.chart_start) { + const fToday = moment().format("YYYY-MM-DD"); + task.offset_from = (moment(fToday).diff(task.chart_start, "days")) * 35; + } + + return task; + } +} diff --git a/worklenz-backend/src/controllers/tasks-controller-v2.ts b/worklenz-backend/src/controllers/tasks-controller-v2.ts new file mode 100644 index 00000000..2b4ad020 --- /dev/null +++ b/worklenz-backend/src/controllers/tasks-controller-v2.ts @@ -0,0 +1,482 @@ +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 TasksControllerBase, {GroupBy, ITaskGroup} from "./tasks-controller-base"; + +export class TaskListGroup implements ITaskGroup { + name: string; + category_id: string | null; + color_code: string; + start_date?: string; + end_date?: string; + todo_progress: number; + doing_progress: number; + done_progress: number; + tasks: any[]; + + constructor(group: any) { + this.name = group.name; + this.category_id = group.category_id || null; + this.start_date = group.start_date || null; + this.end_date = group.end_date || null; + this.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA; + this.todo_progress = 0; + this.doing_progress = 0; + this.done_progress = 0; + this.tasks = []; + } +} + +export default class TasksControllerV2 extends TasksControllerBase { + private static isCountsOnly(query: ParsedQs) { + return query.count === "true"; + } + + public static isTasksOnlyReq(query: ParsedQs) { + return TasksControllerV2.isCountsOnly(query) || query.parent_task; + } + + private static flatString(text: string) { + return (text || "").split(" ").map(s => `'${s}'`).join(","); + } + + private static getFilterByStatusWhereClosure(text: string) { + return text ? `status_id IN (${this.flatString(text)})` : ""; + } + + private static getFilterByPriorityWhereClosure(text: string) { + return text ? `priority_id IN (${this.flatString(text)})` : ""; + } + + private static getFilterByLabelsWhereClosure(text: string) { + return text + ? `id IN (SELECT task_id FROM task_labels WHERE label_id IN (${this.flatString(text)}))` + : ""; + } + + 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 getFilterByProjectsWhereClosure(text: string) { + return text ? `project_id IN (${this.flatString(text)})` : ""; + } + + private static getFilterByAssignee(filterBy: string) { + return filterBy === "member" + ? `id IN (SELECT task_id FROM tasks_assignees WHERE team_member_id = $1)` + : "project_id = $1"; + } + + 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} = TasksControllerV2.toPaginationOptions(options, searchField); + + const isSubTasks = !!options.parent_task; + + const sortFields = sortField.replace(/ascend/g, "ASC").replace(/descend/g, "DESC") || "sort_order"; + + // Filter tasks by statuses + const statusesFilter = TasksControllerV2.getFilterByStatusWhereClosure(options.statuses as string); + // Filter tasks by labels + const labelsFilter = TasksControllerV2.getFilterByLabelsWhereClosure(options.labels as string); + // Filter tasks by its members + const membersFilter = TasksControllerV2.getFilterByMembersWhereClosure(options.members as string); + // Filter tasks by projects + const projectsFilter = TasksControllerV2.getFilterByProjectsWhereClosure(options.projects as string); + // Filter tasks by priorities + const priorityFilter = TasksControllerV2.getFilterByPriorityWhereClosure(options.priorities as string); + // Filter tasks by a single assignee + const filterByAssignee = TasksControllerV2.getFilterByAssignee(options.filterBy as string); + // Returns statuses of each task as a json array if filterBy === "member" + const statusesQuery = TasksControllerV2.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), + (isSubTasks ? "$1 = $1" : filterByAssignee), // ignored filter by member in peoples page for sub-tasks + statusesFilter, + priorityFilter, + labelsFilter, + membersFilter, + projectsFilter + ].filter(i => !!i).join(" AND "); + + return ` + SELECT id, + name, + CONCAT((SELECT key FROM projects WHERE id = t.project_id), '-', task_no) AS task_key, + (SELECT name FROM projects WHERE id = t.project_id) AS project_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.description, + 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 project_phases + WHERE id = (SELECT phase_id FROM task_phase WHERE task_id = t.id)) AS phase_color_code, + + (EXISTS(SELECT 1 FROM task_subscribers WHERE task_id = t.id)) AS has_subscribers, + + (SELECT start_time + FROM task_timers + WHERE task_id = t.id + AND user_id = '${userId}') AS timer_start_time, + + (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, + + (SELECT COUNT(*) FROM task_comments WHERE task_id = t.id) AS comments_count, + (SELECT COUNT(*) FROM task_attachments WHERE task_id = t.id) AS attachments_count, + (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 COALESCE(JSON_AGG(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 = t.id) r) AS labels, + + (SELECT name FROM users WHERE id = t.reporter_id) AS reporter, + (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, + (SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id) AS total_minutes_spent, + created_at, + updated_at, + completed_at, + start_date, + END_DATE ${statusesQuery} + FROM tasks t + WHERE ${filters} ${searchQuery} + ORDER BY ${sortFields} + `; + } + + 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, sort_index + FROM project_phases + WHERE project_id = $1 + ORDER BY sort_index DESC; + `; + 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 = TasksControllerV2.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 TaskListGroup(group); + return g; + }, {}); + + this.updateMapByGroup(tasks, groupBy, map); + + const updatedGroups = Object.keys(map).map(key => { + const group = map[key]; + + TasksControllerV2.updateTaskProgresses(group); + + // if (groupBy === GroupBy.PHASE) + // group.color_code = group.color_code + 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]: ITaskGroup }) { + let index = 0; + const unmapped = []; + for (const task of tasks) { + task.index = index++; + TasksControllerV2.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: "#fbc84c69", + tasks: unmapped + }; + } + } + + public static updateTaskProgresses(group: ITaskGroup) { + const todoCount = group.tasks.filter(t => t.status_category?.is_todo).length; + const doingCount = group.tasks.filter(t => t.status_category?.is_doing).length; + const doneCount = group.tasks.filter(t => t.status_category?.is_done).length; + + const total = group.tasks.length; + + group.todo_progress = +this.calculateTaskCompleteRatio(todoCount, total); + group.doing_progress = +this.calculateTaskCompleteRatio(doingCount, total); + group.done_progress = +this.calculateTaskCompleteRatio(doneCount, total); + } + + @HandleExceptions() + public static async getTasksOnly(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const isSubTasks = !!req.query.parent_task; + const q = TasksControllerV2.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) { + TasksControllerV2.updateTaskViewModel(task); + } + } + + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async convertToTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + UPDATE tasks + SET parent_task_id = NULL, + sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0) + WHERE id = $1; + `; + await db.query(q, [req.body.id, req.body.project_id]); + + const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]); + const [data] = result.rows; + const model = TasksControllerV2.updateTaskViewModel(data.task); + return res.status(200).send(new ServerResponse(true, model)); + } + + @HandleExceptions() + public static async getNewKanbanTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const {id} = req.params; + const result = await db.query("SELECT get_single_task($1) AS task;", [id]); + const [data] = result.rows; + const task = TasksControllerV2.updateTaskViewModel(data.task); + return res.status(200).send(new ServerResponse(true, task)); + } + + @HandleExceptions() + public static async convertToSubtask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + + const groupType = req.body.group_by; + let q = ``; + + if (groupType == "status") { + q = ` + UPDATE tasks + SET parent_task_id = $3, + sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0), + status_id = $4 + WHERE id = $1; + `; + } else if (groupType == "priority") { + q = ` + UPDATE tasks + SET parent_task_id = $3, + sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0), + priority_id = $4 + WHERE id = $1; + `; + } else if (groupType === "phase") { + await db.query(` + UPDATE tasks + SET parent_task_id = $3, + sort_order = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = $2), 0) + WHERE id = $1; + `, [req.body.id, req.body.project_id, req.body.parent_task_id]); + q = `SELECT handle_on_task_phase_change($1, $2);`; + } + + if (req.body.to_group_id === UNMAPPED) + req.body.to_group_id = null; + + const params = groupType === "phase" + ? [req.body.id, req.body.to_group_id] + : [req.body.id, req.body.project_id, req.body.parent_task_id, req.body.to_group_id]; + await db.query(q, params); + + const result = await db.query("SELECT get_single_task($1) AS task;", [req.body.id]); + const [data] = result.rows; + const model = TasksControllerV2.updateTaskViewModel(data.task); + return res.status(200).send(new ServerResponse(true, model)); + } + + public static async getTaskSubscribers(taskId: string) { + const q = ` + SELECT u.name, u.avatar_url, ts.user_id, ts.team_member_id, ts.task_id + FROM task_subscribers ts + LEFT JOIN users u ON ts.user_id = u.id + WHERE ts.task_id = $1; + `; + const result = await db.query(q, [taskId]); + + for (const member of result.rows) + member.color_code = getColor(member.name); + + return this.createTagList(result.rows); + } + + public static async checkUserAssignedToTask(taskId: string, userId: string, teamId: string) { + const q = ` + SELECT EXISTS( + SELECT * FROM tasks_assignees WHERE task_id = $1 AND team_member_id = (SELECT team_member_id FROM team_member_info_view WHERE user_id = $2 AND team_id = $3) + ); + `; + const result = await db.query(q, [taskId, userId, teamId]); + const [data] = result.rows; + + return data.exists; + + } + + @HandleExceptions() + public static async getSubscribers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const subscribers = await this.getTaskSubscribers(req.params.id); + return res.status(200).send(new ServerResponse(true, subscribers)); + } +} diff --git a/worklenz-backend/src/controllers/tasks-controller.ts b/worklenz-backend/src/controllers/tasks-controller.ts new file mode 100644 index 00000000..fd8c6930 --- /dev/null +++ b/worklenz-backend/src/controllers/tasks-controller.ts @@ -0,0 +1,619 @@ +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 { TASK_STATUS_COLOR_ALPHA } from "../shared/constants"; +import { getDates, getMinMaxOfTaskDates, getMonthRange, getWeekRange } from "../shared/tasks-controller-utils"; +import { getColor, getRandomColorCode, log_error, toMinutes } from "../shared/utils"; +import WorklenzControllerBase from "./worklenz-controller-base"; +import HandleExceptions from "../decorators/handle-exceptions"; +import { NotificationsService } from "../services/notifications/notifications.service"; +import { getTaskCompleteInfo } from "../socket.io/commands/on-quick-task"; +import { getAssignees, getTeamMembers } from "../socket.io/commands/on-quick-assign-or-remove"; +import TasksControllerV2 from "./tasks-controller-v2"; +import { IO } from "../shared/io"; +import { SocketEvents } from "../socket.io/events"; +import TasksControllerBase from "./tasks-controller-base"; +import { insertToActivityLogs, logStatusChange } from "../services/activity-logs/activity-logs.service"; +import { forEach } from "lodash"; +import { IActivityLog } from "../services/activity-logs/interfaces"; + +export default class TasksController extends TasksControllerBase { + private static notifyProjectUpdates(socketId: string, projectId: string) { + IO.getSocketById(socketId) + ?.to(projectId) + .emit(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString()); + } + + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT create_task($1) AS task;`; + const result = await db.query(q, [JSON.stringify(req.body)]); + const [data] = result.rows; + + const userId = req.user?.id as string; + + for (const member of data?.task.assignees || []) { + NotificationsService.createTaskUpdate( + "ASSIGN", + userId, + data.task.id, + member.user_id, + member.team_id + ); + } + + return res.status(200).send(new ServerResponse(true, data.task)); + } + + @HandleExceptions() + public static async getGanttTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_gantt_tasks($1) AS gantt_tasks;`; + const result = await db.query(q, [req.user?.id ?? null]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data.gantt_tasks)); + } + + private static sendAssignmentNotifications(task: any, userId: string) { + const newMembers = task.new_assignees.filter((member1: any) => { + return !task.old_assignees.some((member2: any) => { + return member1.team_member_id === member2.team_member_id; + }); + }); + const removedMembers = task.old_assignees.filter((member1: any) => { + return !task.new_assignees.some((member2: any) => { + return member1.team_member_id === member2.team_member_id; + }); + }); + + for (const member of newMembers) { + NotificationsService.createTaskUpdate( + "ASSIGN", + userId, + task.id, + member.user_id, + member.team_id + ); + } + + for (const member of removedMembers) { + NotificationsService.createTaskUpdate( + "UNASSIGN", + userId, + task.id, + member.user_id, + member.team_id + ); + } + } + + public static async notifyStatusChange(userId: string, taskId: string, statusId: string) { + try { + const q2 = "SELECT handle_on_task_status_change($1, $2, $3) AS res;"; + const results1 = await db.query(q2, [userId, taskId, statusId]); + const [d] = results1.rows; + const changeResponse = d.res; + + // notify to all task members of the change + for (const member of changeResponse.members || []) { + if (member.user_id === userId) continue; + NotificationsService.createNotification({ + userId: member.user_id, + teamId: member.team_id, + socketId: member.socket_id, + message: changeResponse.message, + taskId, + projectId: changeResponse.project_id + }); + } + } catch (error) { + log_error(error); + } + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const userId = req.user?.id as string; + + await this.notifyStatusChange(userId, req.body.id, req.body.status_id); + + const q = `SELECT update_task($1) AS task;`; + const result = await db.query(q, [JSON.stringify(req.body)]); + const [data] = result.rows; + const task = data.task || null; + + if (task) { + this.sendAssignmentNotifications(task, userId); + } + + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async updateDuration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { id } = req.params; + const { start, end } = req.body; + + const q = ` + UPDATE tasks + SET start_date = ($1)::TIMESTAMP, + end_date = ($2)::TIMESTAMP + WHERE id = ($3)::UUID + RETURNING id; + `; + const result = await db.query(q, [start, end, id]); + const [data] = result.rows; + if (data?.id) + return res.status(200).send(new ServerResponse(true, {})); + return res.status(200).send(new ServerResponse(false, {}, "Task update failed!")); + } + + @HandleExceptions() + public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { status_id, task_id } = req.params; + const { project_id, from_index, to_index } = req.body; + + const q = `SELECT update_task_status($1, $2, $3, $4, $5) AS status;`; + const result = await db.query(q, [task_id, project_id, status_id, from_index, to_index]); + const [data] = result.rows; + if (data?.status) return res.status(200).send(new ServerResponse(true, {})); + + return res.status(200).send(new ServerResponse(false, {}, "Task update failed!")); + } + + @HandleExceptions() + public static async getTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { id } = req.params; + const q = `SELECT get_project_gantt_tasks($1) AS gantt_tasks;`; + const result = await db.query(q, [id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data?.gantt_tasks)); + } + + @HandleExceptions() + public static async getTasksBetweenRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { project_id, start_date, end_date } = req.query; + const q = ` + SELECT pm.id, + (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]' ::JSON) + FROM (SELECT t.id, + t.name, + t.start_date, + t.project_id, + t.priority_id, + t.done, + t.end_date, + (SELECT color_code + FROM projects + WHERE projects.id = t.project_id) AS color_code, + (SELECT name FROM task_statuses WHERE id = t.status_id) AS status + FROM tasks_assignees ta, + tasks t + WHERE t.archived IS FALSE + AND ta.project_member_id = pm.id + AND t.id = ta.task_id + AND start_date IS NOT NULL + AND end_date IS NOT NULL + ORDER BY start_date) rec) AS tasks + FROM project_members pm + WHERE project_id = $1; + `; + const result = await db.query(q, [project_id]); + const obj: any = {}; + + const minMaxDates: { min_date: string, max_date: string } = await getMinMaxOfTaskDates(project_id as string); + + const dates = await getDates(minMaxDates.min_date || start_date as string, minMaxDates.max_date || end_date as string); + const months = await getWeekRange(dates); + + for (const element of result.rows) { + obj[element.id] = element.tasks; + for (const task of element.tasks) { + const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days")); + const max: number = dates.findIndex((date) => 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, { tasks: [obj], dates, months })); + } + + @HandleExceptions() + public static async getGanttTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT id, + name, + start_date, + project_id, + priority_id, + done, + end_date, + (SELECT color_code + FROM projects + WHERE projects.id = project_id) AS color_code, + (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status, + parent_task_id, + 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('*')::INT FROM tasks WHERE parent_task_id = tasks.id) AS sub_tasks_count + FROM tasks + WHERE archived IS FALSE + AND project_id = $1 + AND parent_task_id IS NULL + ORDER BY start_date; + `; + const result = await db.query(q, [req.query.project_id]); + + const minMaxDates: { + min_date: string, + max_date: string + } = await getMinMaxOfTaskDates(req.query.project_id as string); + + if (!minMaxDates.max_date && !minMaxDates.min_date) { + minMaxDates.min_date = moment().format(); + minMaxDates.max_date = moment().add(45, "days").format(); + } + + const dates = await getDates(minMaxDates.min_date, minMaxDates.max_date); + const weeks = await getWeekRange(dates); + const months = await getMonthRange(dates); + + for (const task of result.rows) { + const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days")); + const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days")); + task.show_sub_tasks = false; + task.sub_tasks = []; + task.min = min + 1; + task.max = max > 0 ? max + 2 : max; + } + + return res.status(200).send(new ServerResponse(true, { tasks: result.rows, dates, weeks, months })); + } + + @HandleExceptions() + public static async getProjectTasksByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_resource_gantt_tasks($1) AS gantt_tasks;`; + const result = await db.query(q, [req.user?.id ?? null]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data?.gantt_tasks)); + } + + @HandleExceptions() + public static async getSelectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_selected_tasks($1) AS tasks`; + const result = await db.query(q, [req.params.id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data?.tasks)); + } + + @HandleExceptions() + public static async getUnselectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_unselected_tasks($1) AS tasks`; + const result = await db.query(q, [req.params.id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data?.tasks)); + } + + /** Should migrate getProjectTasksByStatus to this */ + @HandleExceptions() + public static async getProjectTasksByStatusV2(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + + // Get all statuses + const q1 = ` + 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 result1 = await db.query(q1, [req.query.project, req.user?.team_id]); + const statuses = result1.rows; + + const dataset = []; + + // Query tasks of statuses + for (const status of statuses) { + const q2 = `SELECT get_tasks_by_status($1, $2) AS tasks`; + const result2 = await db.query(q2, [req.params.id, status]); + const [data] = result2.rows; + + for (const task of data.tasks) { + task.name_color = getColor(task.name); + task.names = this.createTagList(task.assignees); + task.names.map((a: any) => a.color_code = getColor(a.name)); + } + dataset.push(data); + } + + return res.status(200).send(new ServerResponse(true, dataset)); + } + + @HandleExceptions() + public static async getProjectTasksByStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_tasks_by_status($1,$2) AS tasks`; + const result = await db.query(q, [req.params.id, req.query.status]); + const [data] = result.rows; + + for (const task of data.tasks) { + task.name_color = getColor(task.name); + task.names = this.createTagList(task.assignees); + task.all_labels = task.labels; + task.labels = this.createTagList(task.labels, 3); + task.names.map((a: any) => a.color_code = getColor(a.name)); + } + + return res.status(200).send(new ServerResponse(true, data?.tasks)); + } + + @HandleExceptions() + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `DELETE + FROM tasks + WHERE 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 getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_task_form_view_model($1, $2, $3, $4) AS view_model;`; + const result = await db.query(q, [req.user?.id ?? null, req.user?.team_id ?? null, req.query.task_id ?? null, (req.query.project_id as string) || null]); + const [data] = result.rows; + + const default_model = { + task: {}, + priorities: [], + projects: [], + statuses: [], + team_members: [], + }; + + const task = data.view_model.task || null; + + if (!task) + return res.status(200).send(new ServerResponse(true, default_model)); + + if (data.view_model && task) { + task.assignees.map((a: any) => { + a.color_code = getColor(a.name); + return a; + }); + + task.names = WorklenzControllerBase.createTagList(task.assignees); + + const totalMinutes = task.total_minutes; + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + task.total_hours = hours; + task.total_minutes = minutes; + task.assignees = (task.assignees || []).map((i: any) => i.team_member_id); + + task.timer_start_time = moment(task.timer_start_time).valueOf(); + + task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA; + } + + for (const member of (data.view_model?.team_members || [])) { + member.color_code = getColor(member.name); + } + + const t = await getTaskCompleteInfo(task); + const info = await TasksControllerV2.getTaskCompleteRatio(t.parent_task_id || t.id); + + if (info) { + t.complete_ratio = info.ratio; + t.completed_count = info.total_completed; + t.total_tasks_count = info.total_tasks; + } + + data.view_model.task = t; + + return res.status(200).send(new ServerResponse(true, data.view_model || default_model)); + } + + @HandleExceptions() + public static async createQuickTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT create_quick_task($1) AS task_id;`; + req.body.reporter_id = req.user?.id ?? null; + req.body.team_id = req.user?.team_id ?? null; + req.body.total_minutes = toMinutes(req.body.total_hours, req.body.total_minutes); + const result = await db.query(q, [JSON.stringify(req.body)]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async createHomeTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT create_home_task($1);`; + let endDate = req.body.end_date; + switch (endDate) { + case "Today": + endDate = moment().format(); + break; + case "Tomorrow": + endDate = moment().add(1, "days").format(); + break; + case "Next Week": + endDate = moment().add(1, "weeks").endOf("isoWeek").format(); + break; + case "Next Month": + endDate = moment().add(1, "months").endOf("month").format(); + break; + case "No Due Date": + endDate = null; + break; + default: + endDate = null; + } + req.body.end_date = endDate; + req.body.reporter_id = req.user?.id ?? null; + req.body.team_id = req.user?.team_id ?? null; + const result = await db.query(q, [JSON.stringify(req.body)]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data.create_home_task.task)); + } + + @HandleExceptions() + public static async bulkChangeStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT bulk_change_tasks_status($1, $2) AS task;`; + const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); + const [data] = result.rows; + + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async bulkChangePriority(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT bulk_change_tasks_priority($1, $2) AS task;`; + const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); + const [data] = result.rows; + + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async bulkChangePhase(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT bulk_change_tasks_phase($1, $2) AS task;`; + const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]); + const [data] = result.rows; + + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + + 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_tasks($1) AS task;`; + await db.query(q, [JSON.stringify(req.body)]); + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + return res.status(200).send(new ServerResponse(true, result)); + } + + @HandleExceptions() + public static async bulkArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT bulk_archive_tasks($1) AS task;`; + req.body.type = req.query.type; + await db.query(q, [JSON.stringify(req.body)]); + const tasks = req.body.tasks.map((t: any) => t.id); + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + return res.status(200).send(new ServerResponse(true, tasks)); + } + + @HandleExceptions() + public static async bulkAssignMe(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + + req.body.team_id = req.user?.team_id; + req.body.user_id = req.user?.id; + + const [task] = req.body.tasks || []; + + const q = `SELECT bulk_assign_to_me($1) AS task;`; + await db.query(q, [JSON.stringify(req.body)]); + + const assignees = await getAssignees(task.id); + const members = await getTeamMembers(req.body.team_id); + // for inline display + const names = WorklenzControllerBase.createTagList(assignees); + + const data = { id: task.id, members, assignees, names }; + + const activityLog: IActivityLog = { + task_id: task.id, + attribute_type: "assignee", + user_id: req.user?.id, + log_type: "assign", + old_value: null, + new_value: req.user?.id, + next_string: req.user?.name + }; + + insertToActivityLogs(activityLog); + + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async bulkAssignLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + + if (req.body.text) { + const q0 = `SELECT bulk_assign_or_create_label($1) AS label;`; + + req.body.team_id = req.user?.team_id; + req.body.color = getRandomColorCode(); + + await db.query(q0, [JSON.stringify(req.body)]); + } else { + const q = `SELECT bulk_assign_label($1, $2) AS task;`; + await db.query(q, [JSON.stringify(req.body), req.user?.id as string]); + } + + TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string); + return res.status(200).send(new ServerResponse(true, null)); + } + + @HandleExceptions() + public static async bulkAssignMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { tasks, members, project_id } = req.body; + try { + for (const task of tasks) { + for (const member of members) { + await TasksController.createTaskBulkAssignees(member.id, project_id, task.id, req.user?.id as string); + } + } + TasksController.notifyProjectUpdates(req.user?.socket_id as string, project_id as string); + return res.status(200).send(new ServerResponse(true, null)); + } catch (error) { + return res.status(500).send(new ServerResponse(false, "An error occurred")); + } + } + + public static async createTaskAssignee(memberId: string, projectId: string, taskId: string, userId: string) { + const q = `SELECT create_task_assignee($1,$2,$3,$4)`; + const result = await db.query(q, [memberId, projectId, taskId, userId]); + return result.rows; + } + + public static async createTaskBulkAssignees(memberId: string, projectId: string, taskId: string, userId: string) { + const q = `SELECT create_bulk_task_assignees($1,$2,$3,$4)`; + const result = await db.query(q, [memberId, projectId, taskId, userId]); + return result.rows; + } + + @HandleExceptions() + public static async getProjectTaskAssignees(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT project_members.team_member_id AS id, + tmiv.name, + tmiv.email, + tmiv.avatar_url + FROM project_members + LEFT JOIN team_member_info_view tmiv ON project_members.team_member_id = tmiv.team_member_id + WHERE project_id = $1 + AND EXISTS(SELECT 1 FROM tasks_assignees WHERE project_member_id = project_members.id); + `; + const result = await db.query(q, [req.params.id]); + + for (const member of result.rows) { + member.color_code = getColor(member.name); + } + + return res.status(200).send(new ServerResponse(true, result.rows)); + } +} diff --git a/worklenz-backend/src/controllers/tasks-custom-columns-controller.ts b/worklenz-backend/src/controllers/tasks-custom-columns-controller.ts new file mode 100644 index 00000000..626fe1f9 --- /dev/null +++ b/worklenz-backend/src/controllers/tasks-custom-columns-controller.ts @@ -0,0 +1,52 @@ +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"; + +export default class TasksCustomColumnsController extends WorklenzControllerBase { + + // Columns + + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async delete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + // Options + + @HandleExceptions() + public static async createOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async getOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async updateOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } + + @HandleExceptions() + public static async deleteOption(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + return res.status(200).send(new ServerResponse(true, [])); + } +} diff --git a/worklenz-backend/src/controllers/team-members-controller.ts b/worklenz-backend/src/controllers/team-members-controller.ts new file mode 100644 index 00000000..12c35bfd --- /dev/null +++ b/worklenz-backend/src/controllers/team-members-controller.ts @@ -0,0 +1,905 @@ +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 { IPassportSession } from "../interfaces/passport-session"; +import { ServerResponse } from "../models/server-response"; +import { sendInvitationEmail } from "../shared/email-templates"; +import { IO } from "../shared/io"; +import { SocketEvents } from "../socket.io/events"; +import WorklenzControllerBase from "./worklenz-controller-base"; +import HandleExceptions from "../decorators/handle-exceptions"; +import { formatDuration, getColor } from "../shared/utils"; +import { TEAM_MEMBER_TREE_MAP_COLOR_ALPHA } from "../shared/constants"; +import { NotificationsService } from "../services/notifications/notifications.service"; + +export default class TeamMembersController 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 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; + } + + public static async createOrInviteMembers(body: T, user: IPassportSession): Promise> { + const q = `SELECT create_team_member($1) AS new_members;`; + const result = await db.query(q, [JSON.stringify(body)]); + + const [data] = result.rows; + const newMembers = data?.new_members || []; + + + const projectId = (body as any)?.project_id; + + NotificationsService.sendTeamMembersInvitations(newMembers, user, projectId || ""); + + return newMembers; + } + + @HandleExceptions({ + raisedExceptions: { + "ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.` + } + }) + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + req.body.team_id = req.user?.team_id || null; + + if (!req.user?.team_id) { + return res.status(200).send(new ServerResponse(false, "Required fields are missing.")); + } + + /** + * Creates or invites new members based on the request body and user information. + * Sends a response with the result. + */ + const newMembers = await this.createOrInviteMembers(req.body, req.user); + return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent")); + } + + @HandleExceptions() + public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + req.query.field = ["is_owner", "active", "u.name", "u.email"]; + req.query.order = "descend"; + + const { + searchQuery, + sortField, + sortOrder, + size, + offset + } = this.toPaginationOptions(req.query, ["u.name", "u.email"], true); + + const paginate = req.query.all === "false" ? `LIMIT ${size} OFFSET ${offset}` : ""; + + const q = ` + 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(*) + FROM project_members + WHERE team_member_id = team_members.id) AS projects_count, + (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 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, + active + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.id + WHERE ${searchQuery} team_id = $1 + ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.id + WHERE ${searchQuery} team_id = $1 + `; + const result = await db.query(q, [req.user?.team_id || null]); + const [members] = result.rows; + + members.data?.map((a: any) => { + a.color_code = getColor(a.name); + return a; + }); + + return res.status(200).send(new ServerResponse(true, members || this.paginatedDatasetDefaultStruct)); + } + + @HandleExceptions() + public static async getAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT get_team_members($1, $2) AS members;`; + const result = await db.query(q, [req.user?.team_id || null, req.query.project || null]); + + const [data] = result.rows; + const members = data?.members || []; + + for (const member of members) { + member.color_code = getColor(member.name); + member.usage = +member.usage; + } + + return res.status(200).send(new ServerResponse(true, members)); + } + + @HandleExceptions() + public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT id, + created_at, + updated_at, + (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = team_members.id), + (SELECT avatar_url FROM users WHERE id = team_members.user_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 name FROM job_titles WHERE id = team_members.job_title_id) AS job_title, + COALESCE( + (SELECT email FROM users WHERE id = team_members.user_id), + (SELECT email + FROM email_invitations + WHERE email_invitations.team_member_id = team_members.id + AND email_invitations.team_id = team_members.team_id + LIMIT 1) + ) AS email, + EXISTS(SELECT id FROM roles WHERE id = team_members.role_id AND admin_role IS TRUE) AS is_admin + FROM team_members + 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 getTeamMembersByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT project_members.id, + team_member_id, + project_access_level_id, + (SELECT name + FROM project_access_levels + WHERE id = project_access_level_id) AS project_access_level_name, + (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) + 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 project_members.created_at DESC; + `; + const result = await db.query(q, [req.params.id]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + req.body.id = req.params.id; + req.body.team_id = req.user?.team_id || null; + req.body.is_admin = !!req.body.is_admin; + + const q = `SELECT update_team_member($1) AS team_member;`; + const result = await db.query(q, [JSON.stringify(req.body)]); + + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async resend_invitation(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + req.body.team_id = req.user?.team_id || null; + + const q = `SELECT resend_team_invitation($1) AS invitation;`; + const result = await db.query(q, [JSON.stringify(req.body)]); + const [data] = result.rows; + + if (!data?.invitation || !data?.invitation.email) + return res.status(200).send(new ServerResponse(false, null, "Resend failed! Please try again.")); + + const member = data.invitation; + + sendInvitationEmail( + !member.is_new, + req.user as IPassportSession, + !member.is_new ? member.name : member.team_member_id, + member.email, + member.team_member_user_id, + member.name || member.email?.split("@")[0] + ); + + if (member.team_member_id) { + NotificationsService.sendInvitation( + req.user?.id as string, + req.user?.name as string, + req.user?.team_name as string, + req.user?.team_id as string, + member.team_member_id + ); + } + + member.id = member.team_member_id; + + return res.status(200).send(new ServerResponse(true, null, "Invitation resent")); + } + + @HandleExceptions() + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { id } = req.params; + + if (!id || !req.user?.team_id) 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, req.user?.team_id]); + 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)); + + } + + @HandleExceptions() + public static async getOverview(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT (SELECT name FROM projects WHERE id = project_members.project_id) AS name, + (SELECT COUNT(*) FROM tasks_assignees WHERE project_member_id = project_members.id) AS assigned_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 t.archived IS FALSE + 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 t.archived IS FALSE + 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 + + FROM project_members + WHERE team_member_id = $1; + `; + + const result = await db.query(q, [req.params.id]); + + for (const object of result.rows) { + object.progress = + object.assigned_task_count > 0 + ? ( + (object.done_task_count / object.assigned_task_count) * + 100 + ).toFixed(0) + : 0; + } + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async getOverviewChart(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT(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 t.archived IS FALSE AND project_member_id IN + (SELECT id FROM project_members WHERE team_member_id = $1) + AND ts.category_id IN (SELECT id FROM sys_task_status_categories WHERE is_done IS TRUE)) AS done_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 t.archived IS FALSE AND project_member_id IN + (SELECT id FROM project_members WHERE team_member_id = $1) + AND ts.category_id IN + (SELECT id FROM sys_task_status_categories WHERE is_doing IS TRUE OR is_todo IS TRUE)) AS pending_count; + `; + const result = await db.query(q, [req.params.id]); + const [data] = result.rows; + return res.status(200).send(new ServerResponse(true, data)); + } + + @HandleExceptions() + public static async getTeamMembersTreeMap(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { selected, team, archived } = req.query; + + let q = ""; + + if (selected === "time") { + 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 COUNT(*) + FROM project_members + WHERE team_member_id = team_members.id + AND CASE + WHEN ($3 IS TRUE) THEN project_id IS NOT NULL + ELSE project_id NOT IN (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = project_members.project_id + AND archived_projects.user_id = $2) END) AS projects_count, + (SELECT SUM(time_spent) + FROM task_work_log + WHERE user_id = team_members.user_id + AND task_id IN (SELECT id + FROM tasks + WHERE project_id IN (SELECT id + FROM projects + WHERE team_id = $1) + AND CASE + WHEN ($3 IS TRUE) THEN project_id IS NOT NULL + ELSE project_id NOT IN (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = tasks.project_id + AND archived_projects.user_id = $2) END)) AS time_logged, + (SELECT name + FROM team_member_info_view + WHERE team_member_info_view.team_member_id = team_members.id), + (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON) + FROM (SELECT project_id, + (SELECT name + FROM projects + WHERE projects.id = project_members.project_id), + (SELECT SUM(time_spent) + FROM task_work_log + WHERE task_work_log.task_id IN (SELECT id + FROM tasks + WHERE tasks.project_id = project_members.project_id) + AND task_work_log.user_id IN (SELECT user_id + FROM team_members + WHERE team_member_id = team_members.id) + AND task_id IN (SELECT id + FROM tasks + WHERE id = task_work_log.task_id + AND CASE + WHEN ($3 IS TRUE) + THEN project_id IS NOT NULL + ELSE project_id NOT IN (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = tasks.project_id + AND archived_projects.user_id = $2) END)) AS value + FROM project_members + WHERE team_member_id = team_members.id) t) AS projects + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.id + WHERE team_id = $1) t) AS data + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.id + WHERE team_id = $1) rec;`; + } + + if (selected === "tasks") { + 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 COUNT(*) + FROM project_members + WHERE team_member_id = team_members.id + AND CASE + WHEN ($3 IS FALSE) THEN project_id IS NOT NULL + ELSE project_id NOT IN (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = project_members.project_id + AND archived_projects.user_id = $2) END) AS projects_count, + (SELECT COUNT(*) + FROM tasks_assignees + WHERE team_member_id = team_members.id + AND CASE + WHEN ($3 IS FALSE) THEN task_id IN (SELECT id + FROM tasks + WHERE id = tasks_assignees.task_id + AND project_id NOT IN + (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = tasks.project_id + AND archived_projects.user_id = $2)) + ELSE task_id IS NOT NULL END) AS task_count, + (SELECT name + FROM team_member_info_view + WHERE team_member_info_view.team_member_id = team_members.id), + (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON) + FROM (SELECT project_id, + (SELECT name + FROM projects + WHERE projects.id = project_members.project_id), + (SELECT COUNT(*) + FROM tasks_assignees + WHERE project_member_id = project_members.id) AS value + FROM project_members + WHERE team_member_id = team_members.id + AND CASE + WHEN ($3 IS FALSE) THEN project_id IS NOT NULL + ELSE project_id NOT IN (SELECT project_id + FROM archived_projects + WHERE archived_projects.project_id = project_members.project_id + AND archived_projects.user_id = $2) END) t) AS projects + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.id + WHERE team_id = $1) t) AS DATA + FROM team_members + LEFT JOIN users u + ON team_members.user_id = u.id + WHERE team_id = $1) rec`; + } + + const result = await db.query(q, [team, req.user?.id, archived]); + const [data] = result.rows; + + const obj: any[] = []; + + data.team_members.data.forEach((element: { + id: string; + name: string; + projects_count: number; + task_count: number; + projects: any[]; + time_logged: number; + }) => { + obj.push({ + id: element.id, + name: element.name, + value: selected === "time" ? element.time_logged || 1 : element.task_count || 0, + color: getColor(element.name) + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA, + label: selected === "time" + ? formatDuration(moment.duration(element.time_logged || "0", "seconds")) + : `
${element.task_count} total tasks`, + labelToolTip: selected === "time" + ? formatDuration(moment.duration(element.time_logged || "0", "seconds")) + : `
- ${element.projects_count} projects
- ${element.task_count} total tasks
` + }); + if (element.projects.length) { + element.projects.forEach(item => { + obj.push({ + id: item.project_id, + name: item.name, + parent: element.id, + value: item.value || 1, + label: selected === "time" ? formatDuration(moment.duration(item.value || "0", "seconds")) : `${item.value} tasks` + }); + }); + } + }); + data.team_members.data = obj; + + return res.status(200).send(new ServerResponse(true, data.team_members)); + } + + @HandleExceptions() + public static async getProjectsByTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { project, status, startDate, endDate } = req.query; + + let projectsString, statusString, dateFilterString1, dateFilterString2, dateFilterString3 = ""; + + if (project && typeof project === "string") { + const projects = project.split(",").map(s => `'${s}'`).join(","); + projectsString = `AND project_id IN (${projects})`; + } + + if (status && typeof status === "string") { + const statuses = status.split(",").map(s => `'${s}'`).join(","); + statusString = `AND status_id IN (${statuses})`; + } + + if (startDate && endDate) { + dateFilterString1 = `AND twl2.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE) AS total_logged_time`; + dateFilterString2 = `LEFT JOIN tasks t ON p.id = t.project_id LEFT JOIN task_work_log twl ON t.id = twl.task_id`; + dateFilterString3 = `AND twl.user_id = (SELECT user_id FROM team_members WHERE id = project_members.team_member_id) + AND twl.created_at::DATE BETWEEN ${startDate}::DATE AND ${endDate}::DATE;`; + } + + const q = ` + (SELECT color_code, + name, + (SELECT count(*) + FROM tasks_assignees + WHERE project_members.team_member_id = tasks_assignees.team_member_id + AND task_id IN (SELECT id FROM tasks WHERE tasks.project_id = projects.id)) AS task_count, + (SELECT name FROM teams WHERE teams.id = projects.team_id) AS team, + (SELECT sum(time_spent) + FROM task_work_log + WHERE task_id IN (SELECT id + FROM tasks + WHERE tasks.project_id = projects.id + AND task_work_log.user_id = + (SELECT user_id FROM team_members WHERE id = project_members.team_member_id)) ${dateFilterString1}) AS total_logged_time + FROM project_members + LEFT JOIN projects ON project_id = projects.id + ${dateFilterString2} + WHERE team_member_id = $1 ${projectsString} ${statusString} ${dateFilterString3} + ORDER BY name)`; + const result = await db.query(q, [req.params.id]); + + result.rows.forEach((element: { total_logged_time: string; }) => { + element.total_logged_time = formatDuration(moment.duration(element.total_logged_time || "0", "seconds")); + }); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async getTasksByMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT name, + (SELECT COUNT(*) + FROM tasks_assignees + WHERE team_member_info_view.team_member_id = tasks_assignees.team_member_id) ::INT AS y + FROM team_member_info_view + WHERE team_id = $1 + ORDER BY name;`; + const result = await db.query(q, [req.user?.team_id]); + + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + public static async getTeamMemberInsightData(team_id: string | undefined, start: any, end: any, project: any, status: any, searchQuery: string, sortField: string, sortOrder: string, size: any, offset: any, all: any) { + let timeRangeTaskWorkLog = ""; + let projectsFilterString = ""; + let statusFilterString = ""; + + if (start && end) { + timeRangeTaskWorkLog = `AND EXISTS(SELECT id FROM task_work_log + WHERE created_at::DATE BETWEEN '${start}'::DATE AND '${end}'::DATE + AND task_work_log.user_id = u.id)`; + } + + if (project && typeof project === "string") { + const projects = project.split(",").map(s => `'${s}'`).join(","); + projectsFilterString = `AND team_members.id IN (SELECT team_member_id FROM project_members WHERE project_id IN (${projects}))`; + } + + if (status && typeof status === "string") { + const projects = status.split(",").map(s => `'${s}'`).join(","); + statusFilterString = `AND team_members.id IN (SELECT team_member_id + FROM project_members + WHERE project_id IN (SELECT id + FROM projects + WHERE projects.team_id = '${team_id}' + AND status_id IN (${projects})))`; + } + + const paginate = all === "false" ? `LIMIT ${size} OFFSET ${offset}` : ""; + + 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(*) + FROM project_members + WHERE team_member_id = team_members.id) AS projects_count, + (SELECT COUNT(*) + FROM tasks_assignees + WHERE team_member_id = team_members.id) AS task_count, + (SELECT SUM(time_spent) + FROM task_work_log + WHERE task_work_log.user_id = tmiv.user_id + AND task_id IN (SELECT id + FROM tasks + WHERE 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)))) AS member_teams + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog} + LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id + WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString} + ORDER BY ${sortField} ${sortOrder} ${paginate}) t) AS data + FROM team_members + LEFT JOIN users u ON team_members.user_id = u.ID ${timeRangeTaskWorkLog} + LEFT JOIN team_member_info_view tmiv ON team_members.id = tmiv.team_member_id + WHERE team_members.team_id = $1 ${searchQuery} ${timeRangeTaskWorkLog} ${projectsFilterString} ${statusFilterString}) rec; + `; + const result = await db.query(q, [team_id || null]); + const [data] = result.rows; + + return data.team_members; + } + + @HandleExceptions() + public static async getTeamMemberList(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { + searchQuery, + sortField, + sortOrder, + size, + offset + } = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]); + const { start, end, project, status, teamId } = req.query; + + const teamMembers = await this.getTeamMemberInsightData(teamId as string, start, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all); + + teamMembers.data.map((a: any) => { + a.color_code = getColor(a.name); + a.total_logged_time = formatDuration(moment.duration(a.total_logged_time_seconds || "0", "seconds")); + }); + + return res.status(200).send(new ServerResponse(true, teamMembers || this.paginatedDatasetDefaultStruct)); + } + + @HandleExceptions() + public static async getTreeDataByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { selected, id } = req.query; + + let valueString = `(SELECT sum(time_spent) + FROM task_work_log + WHERE task_work_log.task_id IN (SELECT id + FROM tasks + WHERE tasks.project_id = project_members.project_id) + AND task_work_log.user_id IN (SELECT user_id + FROM team_members + WHERE team_member_id = team_members.id))::INT AS value`; + + if (selected === "tasks") { + valueString = `(SELECT count(*) FROM tasks_assignees + WHERE project_member_id = project_members.id)::INT AS value`; + } + + const q = ` + SELECT project_id, + (SELECT name FROM projects WHERE projects.id = project_members.project_id), + (SELECT color_code FROM projects WHERE projects.id = project_members.project_id) AS color, + ${valueString} + FROM project_members + WHERE team_member_id = $1`; + const result = await db.query(q, [id]); + + const obj: any[] = []; + + result.rows.forEach((element: { + project_id: string; + name: string; + value: number; + color: string; + time_logged: number; + }) => { + obj.push({ + name: element.name, + value: element.value || 1, + colorValue: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA, + color: element.color + TEAM_MEMBER_TREE_MAP_COLOR_ALPHA, + label: selected === "tasks" ? `${element.value} tasks` : formatDuration(moment.duration(element.value || "0", "seconds")) + }); + }); + + return res.status(200).send(new ServerResponse(true, obj)); + } + + @HandleExceptions() + public static async exportAllMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { + searchQuery, + sortField, + sortOrder, + size, + offset + } = this.toPaginationOptions(req.query, ["tmiv.name", "tmiv.email", "u.name"]); + const { start, end, project, status } = req.query; + + const teamMembers = await this.getTeamMemberInsightData(req.user?.team_id, start || null, end, project, status, searchQuery, sortField, sortOrder, size, offset, req.query.all); + + const exportDate = moment().format("MMM-DD-YYYY"); + const fileName = `Worklenz - Team Members Export - ${exportDate}`; + const metadata = {}; + const title = ""; + + const workbook = new Excel.Workbook(); + const sheet = workbook.addWorksheet(title); + + sheet.headerFooter = { + firstHeader: title + }; + + sheet.columns = [ + { header: "Name", key: "name", width: 50 }, + { header: "Task Count", key: "task_count", width: 25 }, + { header: "Projects Count", key: "projects_count", width: 25 }, + { header: "Email", key: "email", width: 40 }, + ]; + + sheet.getCell("A1").value = req.user?.team_name; + sheet.mergeCells("A1:D1"); + sheet.getCell("A1").alignment = { horizontal: "center" }; + + sheet.getCell("A2").value = `Exported on (${exportDate})`; + sheet.getCell("A2").alignment = { horizontal: "center" }; + + sheet.getCell("A3").value = `From ${start || "-"} to ${end || "-"}`; + + sheet.getRow(5).values = [ + "Name", + "Task Count", + "Projects Count", + "Email" + ]; + + for (const item of teamMembers.data) { + const data = { + name: item.name, + task_count: item.task_count, + projects_count: item.projects_count, + email: item.email + }; + sheet.addRow(data); + } + + sheet.getCell("A1").style.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "D9D9D9" } + }; + sheet.getCell("A1").font = { + size: 16 + }; + + sheet.getCell("A2").style.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "F2F2F2" } + }; + sheet.getCell("A2").font = { + size: 12 + }; + + sheet.getRow(5).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 exportByMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const exportDate = moment().format("MMM-DD-YYYY"); + const fileName = `Team Members - ${exportDate}`; + const title = ""; + + const workbook = new Excel.Workbook(); + + workbook.addWorksheet(title); + + 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 toggleMemberActiveStatus(req: IWorkLenzRequest, res: IWorkLenzResponse) { + if (!req.user?.team_id) return res.status(200).send(new ServerResponse(false, "Required fields are missing.")); + + const q1 = `SELECT active FROM team_members WHERE id = $1;`; + const result1 = await db.query(q1, [req.params?.id]); + const [status] = result1.rows; + + if (status.active) { + const updateQ1 = `UPDATE users + SET active_team = (SELECT id FROM teams WHERE user_id = users.id ORDER BY created_at DESC LIMIT 1) + WHERE id = (SELECT user_id FROM team_members WHERE id = $1 AND active IS TRUE LIMIT 1);`; + await db.query(updateQ1, [req.params?.id]); + } + + const q = `UPDATE team_members SET active = NOT active WHERE id = $1 RETURNING active;`; + const result = await db.query(q, [req.params?.id]); + const [data] = result.rows; + + return res.status(200).send(new ServerResponse(true, [], `Team member ${data.active ? " activated" : " deactivated"} successfully.`)); + } + + @HandleExceptions({ + raisedExceptions: { + "ERROR_EMAIL_INVITATION_EXISTS": `Team member with email "{0}" already exists.` + } + }) + public static async addTeamMember(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + req.body.team_id = req.params?.id || null; + + if (!req.body.team_id || !req.user?.id) return res.status(200).send(new ServerResponse(false, "Required fields are missing.")); + + const newMembers = await this.createOrInviteMembers(req.body, req.user); + return res.status(200).send(new ServerResponse(true, newMembers, `Your teammates will get an email that gives them access to your team.`).withTitle("Invitations sent")); + } +} diff --git a/worklenz-backend/src/controllers/teams-controller.ts b/worklenz-backend/src/controllers/teams-controller.ts new file mode 100644 index 00000000..2c64e59d --- /dev/null +++ b/worklenz-backend/src/controllers/teams-controller.ts @@ -0,0 +1,104 @@ +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 TeamsController extends WorklenzControllerBase { + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { name } = req.body; + + const checkAvailabilityq = `SELECT * from teams WHERE user_id = $2 AND name = $1`; + const check = await db.query(checkAvailabilityq, [name, req.user?.id]); + + if (check.rows.length) return res.status(200).send(new ServerResponse(false, null, "Team name already exist. Try anothor!")); + + const q = `SELECT create_new_team($1, $2);`; + const result = await db.query(q, [name, req.user?.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, + name, + created_at, + (id = $2) AS active, + (user_id = $1) AS owner, + EXISTS(SELECT 1 + FROM email_invitations + WHERE team_id = teams.id + AND team_member_id = (SELECT id + FROM team_members + WHERE team_members.user_id = $1 + AND team_members.team_id = teams.id)) AS pending_invitation, + (CASE + WHEN user_id = $1 THEN 'You' + ELSE (SELECT name FROM users WHERE id = teams.user_id) END + ) AS owns_by + FROM teams + WHERE user_id = $1 + OR id IN (SELECT team_id FROM team_members WHERE team_members.user_id = $1 + AND team_members.active IS TRUE) + ORDER BY name; + `; + + const result = await db.query(q, [req.user?.id, req.user?.team_id ?? null]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async getTeamInvites(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + SELECT id, + team_id, + team_member_id, + (SELECT name FROM teams WHERE id = team_id) AS team_name, + (SELECT name FROM users WHERE id = (SELECT user_id FROM teams WHERE id = team_id)) AS team_owner + FROM email_invitations + WHERE email = (SELECT email FROM users WHERE id = $1); + `; + + const result = await db.query(q, [req.user?.id]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT accept_invitation($1, $2, $3) AS invitation;`; + const result = await db.query(q, [ + req.user?.email, + req.body.team_member_id, + req.user?.id, + ]); + const [data] = result.rows; + + if (req.body.show_alert) { + return res.status(200).send(new ServerResponse(true, data.invitation, "Team invitation accepted")); + } + return res.status(200).send(new ServerResponse(true, data.invitation)); + } + + @HandleExceptions() + public static async activate(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT activate_team($1, $2)`; + await db.query(q, [req.body.id, req.user?.id ?? null]); + return res.status(200).send(new ServerResponse(true, { subdomain: null })); + } + + @HandleExceptions({ + raisedExceptions: { + "TEAM_NAME_EXISTS_ERROR": "Team name already taken. Please enter a different name." + } + }) + public static async updateNameOnce(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT update_team_name_once($1, $2, $3);`; + const result = await db.query(q, [req.user?.id, req.user?.team_id, req.body.name || null]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } +} diff --git a/worklenz-backend/src/controllers/timezones-controller.ts b/worklenz-backend/src/controllers/timezones-controller.ts new file mode 100644 index 00000000..ede78257 --- /dev/null +++ b/worklenz-backend/src/controllers/timezones-controller.ts @@ -0,0 +1,23 @@ +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 TimezonesController extends WorklenzControllerBase { + @HandleExceptions() + public static async get(_req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `SELECT id, name, abbrev, utc_offset FROM timezones ORDER BY name;`; + 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 users SET timezone_id = $2 WHERE id = $1;`; + const result = await db.query(q, [req.user?.id, req.body.timezone]); + return res.status(200).send(new ServerResponse(true, result.rows, "Timezone updated")); + } +} diff --git a/worklenz-backend/src/controllers/todo-list-controller.ts b/worklenz-backend/src/controllers/todo-list-controller.ts new file mode 100644 index 00000000..b00d93c4 --- /dev/null +++ b/worklenz-backend/src/controllers/todo-list-controller.ts @@ -0,0 +1,88 @@ +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 TodoListController extends WorklenzControllerBase { + @HandleExceptions() + public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + INSERT INTO personal_todo_list (name, description, color_code, user_id, index) + VALUES ($1, $2, $3, $4, ((SELECT index FROM personal_todo_list ORDER BY index DESC LIMIT 1) + 1)); + `; + const result = await db.query(q, [req.body.name, req.body.description, req.body.color_code, req.user?.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 {searchQuery} = this.toPaginationOptions(req.query, ["name", "COALESCE(description, '')"]); + const filterByDone = req.query.showCompleted ? "" : "AND done IS FALSE"; + const q = ` + SELECT id, name, description, color_code, done, created_at, updated_at + FROM personal_todo_list + WHERE user_id = $1 ${filterByDone} ${searchQuery} + ORDER BY created_at DESC; + `; + const result = await db.query(q, [req.user?.id]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `UPDATE personal_todo_list SET done = $3 WHERE id = $1 AND user_id = $2;`; + const result = await db.query(q, [req.params.id, req.user?.id, !!req.body.done]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = ` + UPDATE personal_todo_list + SET done = $3, + name = $4, + description = $5, + color_code = $6 + WHERE id = $1 + AND user_id = $2; + `; + const result = await db.query(q, [req.params.id, req.user?.id, !!req.body.done, req.body.name, req.body.description, req.body.color_code]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async updateIndex(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const from = +(req.body.from || 0); + const to = +(req.body.to || 0); + + if (from === to) + return res.status(200).send(new ServerResponse(true, [])); + + const q = ` + UPDATE personal_todo_list + SET index=_index + FROM (SELECT ROW_NUMBER() OVER ( + ORDER BY index < $2 DESC, index != $3 DESC, index >= $1 DESC, index + ) AS _index, + user_id AS _user_id + FROM personal_todo_list + WHERE user_id = $1 + ORDER BY _user_id) AS _ + WHERE user_id = _user_id + AND index != _index; + `; + const result = await db.query(q, [req.user?.id, from, to]); + return res.status(200).send(new ServerResponse(true, result.rows)); + } + + @HandleExceptions() + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const q = `DELETE FROM personal_todo_list 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)); + } +} diff --git a/worklenz-backend/src/controllers/worklenz-controller-base.ts b/worklenz-backend/src/controllers/worklenz-controller-base.ts new file mode 100644 index 00000000..60d0c998 --- /dev/null +++ b/worklenz-backend/src/controllers/worklenz-controller-base.ts @@ -0,0 +1,69 @@ +import { forEach } from "lodash"; +import {DEFAULT_PAGE_SIZE} from "../shared/constants"; +import {toTsQuery} from "../shared/utils"; + +export default abstract class WorklenzControllerBase { + + protected static get paginatedDatasetDefaultStruct() { + return {total: 0, data: []}; + } + + protected static isValidHost(hostname: string) { + return hostname === "worklenz.com" + || hostname === "www.worklenz.com" + || hostname === "dev.worklenz.com" + || hostname === "uat.worklenz.com"; + } + + public static createTagList(list: Array<{ name?: string; end?: boolean; names?: string[]; }>, max = 4) { + let data = [...(list || [])]; + if (data.length > max) { + const remaining = list.slice(max); + const names = remaining.map(i => i.name); + data = data.slice(0, max); + data.push({name: `+${remaining.length}`, end: true, names: names as string[]}); + } + + return data; + } + + protected static toPaginationOptions(queryParams: any, searchField: string | string[], isMemberFilter = false) { + // Pagination + const size = +(queryParams.size || DEFAULT_PAGE_SIZE); + const index = +(queryParams.index || 1); + const offset = queryParams.search ? 0 : (index - 1) * size; + const paging = queryParams.paging || "true"; + + // let s = ""; + // if (typeof searchField === "string") { + // s = `${searchField} || ' ' || id::TEXT`; + // } else if (Array.isArray(searchField)) { + // s = searchField.join(" || ' ' || "); + // } + + // const search = (queryParams.search as string || "").trim(); + // const searchQuery = search ? `AND TO_TSVECTOR(${s}) @@ TO_TSQUERY('${toTsQuery(search)}')` : ""; + + const search = (queryParams.search as string || "").trim(); + + let s = ""; + if (typeof searchField === "string") { + s = ` ${searchField} ILIKE '%${search}%'`; + } else if (Array.isArray(searchField)) { + s = searchField.map(index => ` ${index} ILIKE '%${search}%'`).join(" OR "); + } + + let searchQuery = ""; + + if (search) { + searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `; + } + + // Sort + const sortField = /null|undefined/.test(queryParams.field as string) ? searchField : queryParams.field; + const sortOrder = queryParams.order === "descend" ? "desc" : "asc"; + + return {searchQuery, sortField, sortOrder, size, offset, paging}; + } + +} diff --git a/worklenz-backend/src/cron_jobs/daily-digest-job.ts b/worklenz-backend/src/cron_jobs/daily-digest-job.ts new file mode 100644 index 00000000..ab60f937 --- /dev/null +++ b/worklenz-backend/src/cron_jobs/daily-digest-job.ts @@ -0,0 +1,58 @@ +import {CronJob} from "cron"; +import moment from "moment"; +import db from "../config/db"; +import {IDailyDigest} from "../interfaces/daily-digest"; +import {sendDailyDigest} from "../shared/email-notifications"; +import {log_error} from "../shared/utils"; +import {getBaseUrl, mapTeams} from "./helpers"; + +// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday. +const TIME = "0 11 */1 * 1-5"; +// const TIME = "0/30 * * * *"; +// const TIME = "* * * * *"; + +const log = (value: any) => console.log("daily-digest-cron-job:", value); + +async function onDailyDigestJobTick() { + try { + log("(cron) Daily digest job started."); + const q = "SELECT get_daily_digest() AS digest;"; + const result = await db.query(q, []); + const [fn] = result.rows; + + const dataset: IDailyDigest[] = fn.digest || []; + + let sentCount = 0; + + for (const digest of dataset) { + digest.greeting = `Hi ${digest.name},`; + digest.note = `Here's your ${moment().format("dddd")} update!`; + digest.base_url = `${getBaseUrl()}/worklenz`; + digest.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`; + + digest.recently_assigned = mapTeams(digest.recently_assigned); + digest.overdue = mapTeams(digest.overdue); + digest.recently_completed = mapTeams(digest.recently_completed); + + if (digest.recently_assigned?.length || digest.overdue?.length || digest.recently_completed?.length) { + sentCount++; + void sendDailyDigest(digest.email as string, digest); + } + } + log(`(cron) Daily digest job ended with ${sentCount} emails.`); + } catch (error) { + log_error(error); + log("(cron) Daily digest job ended with errors."); + } +} + +export function startDailyDigestJob() { + log("(cron) Daily digest job ready."); + const job = new CronJob( + TIME, + () => void onDailyDigestJobTick(), + () => log("(cron) Daily Digest job successfully executed."), + true + ); + job.start(); +} diff --git a/worklenz-backend/src/cron_jobs/helpers.ts b/worklenz-backend/src/cron_jobs/helpers.ts new file mode 100644 index 00000000..86c69e60 --- /dev/null +++ b/worklenz-backend/src/cron_jobs/helpers.ts @@ -0,0 +1,75 @@ +import {ITaskAssignmentModelProject, ITaskAssignmentModelTeam} from "../interfaces/task-assignments-model"; +import {isLocalServer} from "../shared/utils"; + +export function mapMembersWithAnd(members: string) { + const $members = members.split(",").map(m => m.trim()); + if ($members.length > 1) { + const last = $members.pop(); + const end = last ? ` and ${last}` : ""; + return `${$members.join(", ")}${end}`; + } + return ""; +} + +export function getBaseUrl() { + if (isLocalServer()) return `http://${process.env.HOSTNAME}`; + return `https://${process.env.HOSTNAME}`; +} + +function mapMembers(project: ITaskAssignmentModelProject) { + for (const task of project.tasks || []) { + if (task.members) + task.members = mapMembersWithAnd(task.members); + } +} + +function updateUrls(project: ITaskAssignmentModelProject) { + project.url = `${getBaseUrl()}/worklenz/projects/${project.id}`; + if (project.tasks) { + project.tasks = project.tasks.map(task => { + if (task.id) + task.url = `${project.url}?task=${task.id}`; + return task; + }); + } +} + +export function mapTeams(data?: ITaskAssignmentModelTeam[]) { + if (!data) return []; + + const result = []; + for (const item of data || []) { + const projects = item.projects?.filter(project => project.tasks?.length); + for (const project of projects || []) { + if (project.id) { + mapMembers(project); + updateUrls(project); + } + } + + if (projects?.length) { + item.projects = projects; + result.push(item); + } + } + return result; +} + + +export function mapProjects(data?: ITaskAssignmentModelTeam[]) { + if (!data) return []; + + const result = []; + for (const item of data || []) { + const projects = item.projects?.filter(project => project.tasks?.length); + for (const project of projects || []) { + if (project.id) { + mapMembers(project); + updateUrls(project); + result.push(project); + } + } + } + + return result; +} diff --git a/worklenz-backend/src/cron_jobs/index.ts b/worklenz-backend/src/cron_jobs/index.ts new file mode 100644 index 00000000..08a46a0c --- /dev/null +++ b/worklenz-backend/src/cron_jobs/index.ts @@ -0,0 +1,9 @@ +import {startDailyDigestJob} from "./daily-digest-job"; +import {startNotificationsJob} from "./notifications-job"; +import {startProjectDigestJob} from "./project-digest-job"; + +export function startCronJobs() { + startNotificationsJob(); + startDailyDigestJob(); + startProjectDigestJob(); +} diff --git a/worklenz-backend/src/cron_jobs/notifications-job.ts b/worklenz-backend/src/cron_jobs/notifications-job.ts new file mode 100644 index 00000000..b0821fe7 --- /dev/null +++ b/worklenz-backend/src/cron_jobs/notifications-job.ts @@ -0,0 +1,68 @@ +// https://www.npmjs.com/package/cron +// https://crontab.guru/#0_22_*/1_*_* + +import {CronJob} from "cron"; +import db from "../config/db"; +import {ITaskAssignmentsModel} from "../interfaces/task-assignments-model"; +import {sendAssignmentUpdate} from "../shared/email-notifications"; +import {log_error} from "../shared/utils"; +import {getBaseUrl, mapProjects} from "./helpers"; + +const TIME = "*/10 * * * *"; + +const log = (value: any) => console.log("notifications-cron-job:", value); + +function getModel(model: ITaskAssignmentsModel): ITaskAssignmentsModel { + const mappedModel: ITaskAssignmentsModel = {...model}; + + mappedModel.name = mappedModel.name?.split(" ")[0] || ""; + mappedModel.url = `${getBaseUrl()}/worklenz/team/member/${mappedModel.team_member_id}`; + mappedModel.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`; + + const teams = []; + for (const team of model.teams || []) { + team.projects = mapProjects([team]); + if (team.projects.length) + teams.push(team); + } + + mappedModel.teams = teams; + return mappedModel; +} + +async function onNotificationJobTick() { + try { + log("(cron) Notifications job started."); + const q = "SELECT get_task_updates() AS updates;"; + const result = await db.query(q, []); + const [data] = result.rows; + const updates = (data.updates || []) as ITaskAssignmentsModel[]; + + let sentCount = 0; + + for (const item of updates) { + if (item.email) { + const model = getModel(item); + if (model.teams?.length) { + sentCount++; + void sendAssignmentUpdate(item.email, model); + } + } + } + log(`(cron) Notifications job ended with ${sentCount} emails.`); + } catch (error) { + log_error(error); + log("(cron) Notifications job ended with errors."); + } +} + +export function startNotificationsJob() { + log("(cron) Email notifications job ready."); + const job = new CronJob( + TIME, + () => void onNotificationJobTick(), + () => log("(cron) Notifications job successfully executed."), + true + ); + job.start(); +} diff --git a/worklenz-backend/src/cron_jobs/project-digest-job.ts b/worklenz-backend/src/cron_jobs/project-digest-job.ts new file mode 100644 index 00000000..dac17a52 --- /dev/null +++ b/worklenz-backend/src/cron_jobs/project-digest-job.ts @@ -0,0 +1,70 @@ +import {CronJob} from "cron"; +import db from "../config/db"; +import {log_error} from "../shared/utils"; +import {getBaseUrl} from "./helpers"; +import {IProjectDigest, IProjectDigestTask} from "../interfaces/project-digest"; +import {sendProjectDailyDigest} from "../shared/email-notifications"; + +// At 11:00+00 (4.30pm+530) on every day-of-month if it's on every day-of-week from Monday through Friday. +const TIME = "0 11 */1 * 1-5"; +// const TIME = "* * * * *"; + +const log = (value: any) => console.log("project-digest-cron-job:", value); + +function updateTaskUrls(projectId: string, tasks: IProjectDigestTask[]) { + const baseUrl = getBaseUrl(); + for (const task of tasks) { + task.url = `${baseUrl}/worklenz/projects/${projectId}?tab=tasks-list&task=${task.id}`; + } +} + +function updateMetadata(project: IProjectDigest, subscriberName: string) { + project.greeting = `Hi ${subscriberName},`; + project.summary = `Here's the "${project.name}" summary | ${project.team_name}`; + project.settings_url = `${getBaseUrl()}/worklenz/settings/notifications`; + project.project_url = `${getBaseUrl()}/worklenz/projects/${project.id}?tab=tasks-list`; +} + +async function onProjectDigestJobTick() { + try { + log("(cron) Daily digest job started."); + const q = "SELECT get_project_daily_digest() AS digest;"; + const result = await db.query(q, []); + const [fn] = result.rows; + + const dataset: IProjectDigest[] = fn.digest || []; + + let sentCount = 0; + + for (const project of dataset) { + for (const subscriber of project.subscribers) { + updateMetadata(project, subscriber.name); + + updateTaskUrls(project.id, project.today_completed); + updateTaskUrls(project.id, project.today_new); + updateTaskUrls(project.id, project.due_tomorrow); + + if (subscriber.email) { + sentCount++; + void sendProjectDailyDigest(subscriber.email, project); + } + } + } + + log(`(cron) Project digest job ended with ${sentCount} emails.`); + } catch (error) { + log_error(error); + log("(cron) Project digest job ended with errors."); + } +} + +export function startProjectDigestJob() { + log("(cron) Project digest job ready."); + const job = new CronJob( + TIME, + () => void onProjectDigestJobTick(), + () => log("(cron) Project Digest job successfully executed."), + true + ); + job.start(); +} diff --git a/worklenz-backend/src/decorators/handle-exceptions.ts b/worklenz-backend/src/decorators/handle-exceptions.ts new file mode 100644 index 00000000..01a166e7 --- /dev/null +++ b/worklenz-backend/src/decorators/handle-exceptions.ts @@ -0,0 +1,83 @@ +import WorklenzControllerBase from "../controllers/worklenz-controller-base"; +import {ServerResponse} from "../models/server-response"; +import {DEFAULT_ERROR_MESSAGE} from "../shared/constants"; +import {DB_CONSTRAINS} from "../shared/constraints"; +import {log_error} from "../shared/utils"; +import {Response} from "express"; + +interface IExceptionHandlerConfig { + message?: string; + /** e.g. req.user? "user", req.body? "body" */ + logWithError?: string; + /** Throws from postgres functions */ + raisedExceptions?: { [x: string]: string }; +} + +const defaults: IExceptionHandlerConfig = { + message: DEFAULT_ERROR_MESSAGE, + raisedExceptions: {}, + logWithError: "user" +}; + +const isValid = (options: any, key: string) => Object.keys(options[key] || {}).length > 0; +const mergeWithDefaults = (options: any) => ({...defaults, ...(options || {})}); + +function getConstraint(error: any) { + return DB_CONSTRAINS[error?.constraint] ?? null; +} + +function getConstraintResponse(constraint: string) { + if (constraint === "[IGNORE]") + return new ServerResponse(true, null); + return new ServerResponse(false, null, constraint || DEFAULT_ERROR_MESSAGE); +} + +function hasRaisedException(opt: any, keys: any[]): boolean { + return opt.raisedExceptions?.[keys[0]]; +} + +function getExceptionMessage(opt: any, keys: any[]) { + return (opt.raisedExceptions[keys[0]] || DEFAULT_ERROR_MESSAGE).replace(/(\{0\})/g, (keys[1] || "")); +} + +function getKeys(error: any) { + return ((error?.message) || "").split(":"); +} + +function handleError(error: any, res: Response, opt: any, req: any) { + const constraint = getConstraint(error); + if (typeof constraint === "string") { + const response = getConstraintResponse(constraint); + return res.status(200).send(response); + } + + if (isValid(opt, "raisedExceptions")) { + const keys = getKeys(error); + if (hasRaisedException(opt, keys)) { + const msg = getExceptionMessage(opt, keys); + return res.status(200).send(new ServerResponse(false, null, msg)); + } + } + + log_error(error, (opt.logWithError && req[opt.logWithError]) || null); + return res.status(200).send(new ServerResponse(false, null, opt.message)); +} + +/** HandleExceptions can only be used with an instance of WorklenzControllerBase. */ +export default function HandleExceptions(options?: IExceptionHandlerConfig) { + const opt = mergeWithDefaults(options); + return (target: any, key: string, descriptor: PropertyDescriptor) => { + if (!(target.prototype instanceof WorklenzControllerBase)) + throw new Error("@HandleExceptions can only be used with an instance of WorklenzControllerBase."); + + const originalMethod = descriptor.value; + descriptor.value = async (...args: any[]) => { + try { + return await originalMethod.apply(target, args); + } catch (error: any) { + const [req, res] = args; + return handleError(error, res, opt, req); + } + }; + }; +} diff --git a/worklenz-backend/src/interfaces/.gitkeep b/worklenz-backend/src/interfaces/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/worklenz-backend/src/interfaces/aws-bounced-email-response.ts b/worklenz-backend/src/interfaces/aws-bounced-email-response.ts new file mode 100644 index 00000000..56a67f1f --- /dev/null +++ b/worklenz-backend/src/interfaces/aws-bounced-email-response.ts @@ -0,0 +1,59 @@ +export interface ISESBouncedRecipient { + emailAddress: string; + action: string; + status: string; + diagnosticCode: string; +} + +export interface ISESBounce { + feedbackId: string; + bounceType: string; + bounceSubType: string; + bouncedRecipients: ISESBouncedRecipient[]; + timestamp: string; + remoteMtaIp: string; + reportingMTA: string; +} + +export interface ISESBouncedHeaders { + name: string; + value: string; +} + +export interface ISESCommonHeaders { + from: string[]; + to: string[]; + subject: string; +} + +export interface ISESBouncedMail { + timestamp: string; + source: string; + sourceArn: string; + sourceIp: string; + callerIdentity: string; + sendingAccountId: string; + messageId: string; + destination: string[]; + headersTruncated: boolean; + headers: ISESBouncedHeaders[]; + commonHeaders: ISESCommonHeaders; +} + +export interface ISESBouncedMessage { + notificationType: string; + bounce: ISESBounce; + mail: ISESBouncedMail; +} + +export interface ISESBouncedEmailResponse { + Type: string; + MessageId: string; + TopicArn: string; + Message: ISESBouncedMessage; + Timestamp: string; + SignatureVersion: string; + Signature: string; + SigningCertURL: string; + UnsubscribeURL: string; +} diff --git a/worklenz-backend/src/interfaces/aws-complaint-email-response.ts b/worklenz-backend/src/interfaces/aws-complaint-email-response.ts new file mode 100644 index 00000000..ea01013d --- /dev/null +++ b/worklenz-backend/src/interfaces/aws-complaint-email-response.ts @@ -0,0 +1,42 @@ +export interface ISESComplaintMail { + timestamp: string; + source: string; + sourceArn: string; + sourceIp: string; + callerIdentity: string; + sendingAccountId: string; + messageId: string; + destination: string[]; +} + +export interface ISESComplaintEmailAddress { + emailAddress: string; +} + +export interface ISESComplaint { + feedbackId: string; + complaintSubType: string | null; + complainedRecipients: ISESComplaintEmailAddress[]; + timestamp: string; + userAgent: string; + complaintFeedbackType: string; + arrivalDate: string; +} + +export interface ISESComplaintMessage { + notificationType: string; + complaint: ISESComplaint; + mail: ISESComplaintMail; +} + +export interface ISESComplaintResponse { + Type: string; + MessageId: string; + TopicArn: string; + Message: ISESComplaintMessage; + Timestamp: string; + SignatureVersion: string; + Signature: string; + SigningCertURL: string; + UnsubscribeURL: string; +} diff --git a/worklenz-backend/src/interfaces/comment-email-notification.ts b/worklenz-backend/src/interfaces/comment-email-notification.ts new file mode 100644 index 00000000..4c5c0999 --- /dev/null +++ b/worklenz-backend/src/interfaces/comment-email-notification.ts @@ -0,0 +1,18 @@ +export interface ICommentEmailNotification { + greeting: string; + summary: string; + team: string; + project_name: string; + comment: string; + task: string; + settings_url: string; + task_url: string; +} + +export interface IProjectCommentEmailNotification { + greeting: string; + summary: string; + team: string; + project_name: string; + comment: string; +} diff --git a/worklenz-backend/src/interfaces/daily-digest.ts b/worklenz-backend/src/interfaces/daily-digest.ts new file mode 100644 index 00000000..7cb777ba --- /dev/null +++ b/worklenz-backend/src/interfaces/daily-digest.ts @@ -0,0 +1,13 @@ +import {ITaskAssignmentModelTeam} from "./task-assignments-model"; + +export interface IDailyDigest { + name?: string; + greeting?: string; + note?: string; + email?: string; + base_url?: string; + settings_url?: string; + recently_assigned?: ITaskAssignmentModelTeam[]; + overdue?: ITaskAssignmentModelTeam[]; + recently_completed?: ITaskAssignmentModelTeam[]; +} diff --git a/worklenz-backend/src/interfaces/deserialize-callback.ts b/worklenz-backend/src/interfaces/deserialize-callback.ts new file mode 100644 index 00000000..f6ed5eb2 --- /dev/null +++ b/worklenz-backend/src/interfaces/deserialize-callback.ts @@ -0,0 +1,5 @@ +import {IPassportSession} from "./passport-session"; + +export interface IDeserializeCallback { + (error: unknown | null, user: IPassportSession | null): void; +} diff --git a/worklenz-backend/src/interfaces/email-template-type.ts b/worklenz-backend/src/interfaces/email-template-type.ts new file mode 100644 index 00000000..e1c94d86 --- /dev/null +++ b/worklenz-backend/src/interfaces/email-template-type.ts @@ -0,0 +1,16 @@ +export enum IEmailTemplateType { + None, + NewSubscriber, + TeamMemberInvitation, + UnregisteredTeamMemberInvitation, + PasswordChange, + Welcome, + OTPVerification, + ResetPassword, + TaskAssigneeChange, + DailyDigest, + TaskDone, + ProjectDailyDigest, + TaskComment, + ProjectComment +} diff --git a/worklenz-backend/src/interfaces/gantt-chart.ts b/worklenz-backend/src/interfaces/gantt-chart.ts new file mode 100644 index 00000000..55e84619 --- /dev/null +++ b/worklenz-backend/src/interfaces/gantt-chart.ts @@ -0,0 +1,24 @@ +export interface IGanttDateRange { + isSunday?: boolean; + isToday?: boolean; + isWeekend?: boolean; + isLastDayOfWeek?: boolean; + isLastDayOfMonth?: boolean; + date?: Date; +} + +export interface IGanttWeekRange { + max?: number; + min?: number; + month_name?: string; + week_index?: number; + days_of_week?: IGanttDateRange[]; +} + +export interface IGanttMonthRange { + max?: number; + min?: number; + month_name?: string; + month_index?: number; + days_of_month?: IGanttDateRange[]; +} diff --git a/worklenz-backend/src/interfaces/passport-session.ts b/worklenz-backend/src/interfaces/passport-session.ts new file mode 100644 index 00000000..dc9f408f --- /dev/null +++ b/worklenz-backend/src/interfaces/passport-session.ts @@ -0,0 +1,20 @@ +import {IUser} from "./user"; + +export interface IPassportSession extends IUser { + id?: string; + email?: string; + name?: string; + owner?: boolean; + team_id?: string; + team_member_id?: string; + team_name?: string; + is_admin?: boolean; + is_member?: boolean; + is_google?: boolean; + build_v?: string; + timezone?: string; + timezone_name?: string; + socket_id?: string; + is_expired?: boolean; + owner_id?: string; +} diff --git a/worklenz-backend/src/interfaces/password-validity-result.ts b/worklenz-backend/src/interfaces/password-validity-result.ts new file mode 100644 index 00000000..8598a011 --- /dev/null +++ b/worklenz-backend/src/interfaces/password-validity-result.ts @@ -0,0 +1,6 @@ +export interface IPasswordValidityResult { + contains: ("lowercase" | "uppercase" | "number" | "symbol")[]; + length: number; + value: number; + text: "Too Weak" | "Weak" | "Strong" | "Excellent"; +} diff --git a/worklenz-backend/src/interfaces/pg-session-data.ts b/worklenz-backend/src/interfaces/pg-session-data.ts new file mode 100644 index 00000000..7ce75071 --- /dev/null +++ b/worklenz-backend/src/interfaces/pg-session-data.ts @@ -0,0 +1,12 @@ +export interface PgSessionData { + cookie: { + originalMaxAge: number; + expires: string; + httpOnly: boolean; + path: string; + }; + flash: object; + passport?: { + user: string; + }; +} diff --git a/worklenz-backend/src/interfaces/project-category.ts b/worklenz-backend/src/interfaces/project-category.ts new file mode 100644 index 00000000..1a827faa --- /dev/null +++ b/worklenz-backend/src/interfaces/project-category.ts @@ -0,0 +1,9 @@ +export interface IProjectCategory { + id?: string; + name?: string; + color_code?: string; + team_id?: string; + created_by?: string; + created_at?: string; + updated_at?: string; +} diff --git a/worklenz-backend/src/interfaces/project-digest.ts b/worklenz-backend/src/interfaces/project-digest.ts new file mode 100644 index 00000000..84fa646a --- /dev/null +++ b/worklenz-backend/src/interfaces/project-digest.ts @@ -0,0 +1,25 @@ +export interface IProjectDigestTask { + id: string; + name: string; + url: string; + members: string; +} + +export interface IProjectDigestSubscriber { + name: string; + email: string; +} + +export interface IProjectDigest { + id: string; + name: string; + team_name: string; + greeting: string; + summary: string; + settings_url: string; + project_url: string; + today_completed: IProjectDigestTask[]; + today_new: IProjectDigestTask[]; + due_tomorrow: IProjectDigestTask[]; + subscribers: IProjectDigestSubscriber[]; +} diff --git a/worklenz-backend/src/interfaces/project-folder.ts b/worklenz-backend/src/interfaces/project-folder.ts new file mode 100644 index 00000000..d3c3d1a7 --- /dev/null +++ b/worklenz-backend/src/interfaces/project-folder.ts @@ -0,0 +1,11 @@ +export interface IProjectFolder { + id: string; + name: string; + key: string; + color_code: string; + created_by: string; + parent_folder_id?: string; + team_id: string; + created_at: string; + updated_at: string; +} diff --git a/worklenz-backend/src/interfaces/serialize-callback.ts b/worklenz-backend/src/interfaces/serialize-callback.ts new file mode 100644 index 00000000..1bea9e09 --- /dev/null +++ b/worklenz-backend/src/interfaces/serialize-callback.ts @@ -0,0 +1,3 @@ +export interface ISerializeCallback { + (error: string | null, id: string | null): void; +} diff --git a/worklenz-backend/src/interfaces/socket-session.ts b/worklenz-backend/src/interfaces/socket-session.ts new file mode 100644 index 00000000..9e5936ed --- /dev/null +++ b/worklenz-backend/src/interfaces/socket-session.ts @@ -0,0 +1,5 @@ +export interface ISocketSession { + session?: { + passport?: { user?: string; } + } +} diff --git a/worklenz-backend/src/interfaces/task-assignments-model.ts b/worklenz-backend/src/interfaces/task-assignments-model.ts new file mode 100644 index 00000000..6a4ead0e --- /dev/null +++ b/worklenz-backend/src/interfaces/task-assignments-model.ts @@ -0,0 +1,30 @@ +export interface ITaskAssignmentModelTask { + id?: string; + task_name?: string; + updater_name?: string; + members?: string; + url?: string; +} + +export interface ITaskAssignmentModelProject { + id?: string; + name?: string; + url?: string; + tasks?: ITaskAssignmentModelTask[]; +} + +export interface ITaskAssignmentModelTeam { + id?: string; + name?: string; + team_member_id?: string; + projects?: ITaskAssignmentModelProject[]; +} + +export interface ITaskAssignmentsModel { + email?: string; + name?: string; + team_member_id?: string; + url?: string; + settings_url?: string; + teams?: ITaskAssignmentModelTeam[]; +} diff --git a/worklenz-backend/src/interfaces/task-moved-to-done.ts b/worklenz-backend/src/interfaces/task-moved-to-done.ts new file mode 100644 index 00000000..0b14c3b1 --- /dev/null +++ b/worklenz-backend/src/interfaces/task-moved-to-done.ts @@ -0,0 +1,14 @@ +export interface TaskRecord { + name: string; + members: string; + url: string; + team_name: string; + project_name: string; +} + +export interface ITaskMovedToDoneRecord { + greeting: string; + summary: string; + settings_url: string; + task: TaskRecord; +} diff --git a/worklenz-backend/src/interfaces/user.ts b/worklenz-backend/src/interfaces/user.ts new file mode 100644 index 00000000..c8badffa --- /dev/null +++ b/worklenz-backend/src/interfaces/user.ts @@ -0,0 +1,6 @@ +export interface IUser { + id?: string; + name?: string; + password?: string; + email?: string; +} diff --git a/worklenz-backend/src/interfaces/worklenz-request.ts b/worklenz-backend/src/interfaces/worklenz-request.ts new file mode 100644 index 00000000..d8722e79 --- /dev/null +++ b/worklenz-backend/src/interfaces/worklenz-request.ts @@ -0,0 +1,6 @@ +import {Request} from "express"; +import {IPassportSession} from "./passport-session"; + +export interface IWorkLenzRequest extends Request { + user?: IPassportSession; +} diff --git a/worklenz-backend/src/interfaces/worklenz-response.ts b/worklenz-backend/src/interfaces/worklenz-response.ts new file mode 100644 index 00000000..44f15ef3 --- /dev/null +++ b/worklenz-backend/src/interfaces/worklenz-response.ts @@ -0,0 +1,3 @@ +import {Response} from "express"; + +export type IWorkLenzResponse = Response diff --git a/worklenz-backend/src/json_schemas/email-request-schema.ts b/worklenz-backend/src/json_schemas/email-request-schema.ts new file mode 100644 index 00000000..f03a8030 --- /dev/null +++ b/worklenz-backend/src/json_schemas/email-request-schema.ts @@ -0,0 +1,13 @@ +export default { + type: "object", + properties: { + to: { + type: "array", + items: {type: "string"}, + minItems: 1 // Optional field, at least 1 item required + }, + subject: {type: "string"}, + html: {type: "string"} + }, + required: ["to", "subject", "html"] // subject and html fields are required +}; diff --git a/worklenz-backend/src/json_schemas/item-create-schema.ts b/worklenz-backend/src/json_schemas/item-create-schema.ts new file mode 100644 index 00000000..c2db6f96 --- /dev/null +++ b/worklenz-backend/src/json_schemas/item-create-schema.ts @@ -0,0 +1,14 @@ +export default { + type: "object", + properties: { + name: {type: "string", message: "Invalid Name"}, + sku: {type: "string", message: "Invalid SKU"}, + stock_amount: {type: "number", message: "Invalid Stock Amount"}, + image: {type: "string", message: "Invalid Image"}, + price: {type: "number", message: "Invalid Price"}, + value: {type: "number", message: "Invalid Value"}, + active: {type: "boolean", message: "Invalid type of Active"}, + category_id: {type: "string", message: "Invalid Category"} + }, + required: ["name", "sku", "stock_amount", "image", "price", "value", "active", "category_id"], +}; diff --git a/worklenz-backend/src/json_schemas/project-category-schema.ts b/worklenz-backend/src/json_schemas/project-category-schema.ts new file mode 100644 index 00000000..b5383bee --- /dev/null +++ b/worklenz-backend/src/json_schemas/project-category-schema.ts @@ -0,0 +1,13 @@ +export default { + type: "object", + properties: { + name: { + type: "string", + minLength: 2, + maxLength: 20, + message: "Invalid category name" + }, + color_code: {type: ["string", "null"]} + }, + required: ["name"] +}; diff --git a/worklenz-backend/src/json_schemas/project-folder-schema.ts b/worklenz-backend/src/json_schemas/project-folder-schema.ts new file mode 100644 index 00000000..b8f66998 --- /dev/null +++ b/worklenz-backend/src/json_schemas/project-folder-schema.ts @@ -0,0 +1,13 @@ +export default { + type: "object", + properties: { + name: { + type: "string", + minLength: 2, + maxLength: 20, + message: "Invalid folder name" + }, + color_code: {type: ["string", "null"]} + }, + required: ["name"] +}; diff --git a/worklenz-backend/src/json_schemas/task-phase-create-schema.ts b/worklenz-backend/src/json_schemas/task-phase-create-schema.ts new file mode 100644 index 00000000..306c340a --- /dev/null +++ b/worklenz-backend/src/json_schemas/task-phase-create-schema.ts @@ -0,0 +1,12 @@ +export default { + type: "object", + properties: { + name: { + type: "string", + message: "Invalid name", + minLength: 2, + maxLength: 30 + } + }, + required: ["name"] +}; diff --git a/worklenz-backend/src/middlewares/image-to-webp.ts b/worklenz-backend/src/middlewares/image-to-webp.ts new file mode 100644 index 00000000..1c49acba --- /dev/null +++ b/worklenz-backend/src/middlewares/image-to-webp.ts @@ -0,0 +1,23 @@ +import {NextFunction} from "express"; +import sharp from "sharp"; + +import {IWorkLenzRequest} from "../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../interfaces/worklenz-response"; +import {ServerResponse} from "../models/server-response"; + +export default async function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) { + if (!req.body.file) return next(); + try { + const buffer = Buffer.from(req.body.file.replace(/^data:(.*?);base64,/, ""), "base64"); + const out = await sharp(buffer) + .webp({quality: 50}) + .toBuffer(); + + req.body.type = "webp"; + req.body.buffer = out; + + return next(); + } catch (error) { + return res.status(200).send(new ServerResponse(false, null, "Upload failed")); + } +} diff --git a/worklenz-backend/src/middlewares/licensing-middleware.ts b/worklenz-backend/src/middlewares/licensing-middleware.ts new file mode 100644 index 00000000..e69de29b diff --git a/worklenz-backend/src/middlewares/map-tasks-to-bulk-update.ts b/worklenz-backend/src/middlewares/map-tasks-to-bulk-update.ts new file mode 100644 index 00000000..da6ff0d4 --- /dev/null +++ b/worklenz-backend/src/middlewares/map-tasks-to-bulk-update.ts @@ -0,0 +1,9 @@ +import {NextFunction} from "express"; +import {IWorkLenzRequest} from "../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../interfaces/worklenz-response"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction) { + // Map string[] -> Array<{ id: string; }> + req.body.tasks = req.body.tasks.map((id: string) => ({id})); + return next(); +} diff --git a/worklenz-backend/src/middlewares/schema-validator.ts b/worklenz-backend/src/middlewares/schema-validator.ts new file mode 100644 index 00000000..6a719f42 --- /dev/null +++ b/worklenz-backend/src/middlewares/schema-validator.ts @@ -0,0 +1,15 @@ +import {NextFunction, Request, Response} from "express"; +import {Schema, Validator} from "jsonschema"; +import {ServerResponse} from "../models/server-response"; + +export default function (schema: Schema) { + return (req: Request, res: Response, next: NextFunction) => { + const validator = new Validator(); + const result = validator.validate(req.body, schema); + + if (!result.valid) + return res.status(400).send(new ServerResponse(false, null, (result.errors[0]?.schema as any).message || null)); + + return next(); + }; +} diff --git a/worklenz-backend/src/middlewares/session-middleware.ts b/worklenz-backend/src/middlewares/session-middleware.ts new file mode 100644 index 00000000..4efb4e04 --- /dev/null +++ b/worklenz-backend/src/middlewares/session-middleware.ts @@ -0,0 +1,26 @@ +import session from "express-session"; +import db from "../config/db"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const pgSession = require("connect-pg-simple")(session); + +export default session({ + name: process.env.SESSION_NAME, + secret: process.env.SESSION_SECRET || [], // session secret + proxy: false, + resave: false, + saveUninitialized: true, + rolling: true, + store: new pgSession({ + pool: db.pool, + tableName: "pg_sessions" + }), + cookie: { + path: "/", + // secure: true, + // httpOnly: true, + // sameSite: true, + // domain: process.env.HOSTNAME, + maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days + } +}); diff --git a/worklenz-backend/src/middlewares/validators/README.md b/worklenz-backend/src/middlewares/validators/README.md new file mode 100644 index 00000000..ffac8921 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/README.md @@ -0,0 +1 @@ +Validators are expressjs middlewares that validate the request body. diff --git a/worklenz-backend/src/middlewares/validators/avatar-validator.ts b/worklenz-backend/src/middlewares/validators/avatar-validator.ts new file mode 100644 index 00000000..a882e5a6 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/avatar-validator.ts @@ -0,0 +1,22 @@ +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 {file, file_name, size} = req.body; + + if (!file || !file_name || !size) + return res.status(200).send(new ServerResponse(false, null, "Upload failed")); + + if (size > 200000) + return res.status(200).send(new ServerResponse(false, null, "Max file size 200kb.").withTitle("Upload failed!")); + + req.body.type = file_name.split(".").pop(); + + if (req.body.type !== "png" && req.body.type !== "jpg" && req.body.type !== "jpeg") + return res.status(200).send(new ServerResponse(false, null, "Invalid file type")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/body-name-validator.ts b/worklenz-backend/src/middlewares/validators/body-name-validator.ts new file mode 100644 index 00000000..08153a3a --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/body-name-validator.ts @@ -0,0 +1,12 @@ +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 {name} = req.body; + if (!name?.trim()) + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/bulk-tasks-phase-validators.ts b/worklenz-backend/src/middlewares/validators/bulk-tasks-phase-validators.ts new file mode 100644 index 00000000..766d9f04 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/bulk-tasks-phase-validators.ts @@ -0,0 +1,13 @@ +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 {phase_id, tasks} = req.body; + if (!phase_id || !Array.isArray(tasks)) + return res.status(400).send(new ServerResponse(false, null)); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/bulk-tasks-priority-validators.ts b/worklenz-backend/src/middlewares/validators/bulk-tasks-priority-validators.ts new file mode 100644 index 00000000..96af46d2 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/bulk-tasks-priority-validators.ts @@ -0,0 +1,13 @@ +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 {priority_id, tasks} = req.body; + if (!priority_id || !Array.isArray(tasks)) + return res.status(400).send(new ServerResponse(false, null)); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/bulk-tasks-status-validator.ts b/worklenz-backend/src/middlewares/validators/bulk-tasks-status-validator.ts new file mode 100644 index 00000000..45becfd2 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/bulk-tasks-status-validator.ts @@ -0,0 +1,13 @@ +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 {status_id, tasks} = req.body; + if (!status_id || !Array.isArray(tasks)) + return res.status(400).send(new ServerResponse(false, null)); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/bulk-tasks-validator.ts b/worklenz-backend/src/middlewares/validators/bulk-tasks-validator.ts new file mode 100644 index 00000000..844142a2 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/bulk-tasks-validator.ts @@ -0,0 +1,15 @@ +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 {tasks} = req.body; + if (!Array.isArray(tasks)) + return res.status(400).send(new ServerResponse(false, null)); + + req.body.labels = Array.isArray(req.body.labels) ? req.body.labels : []; + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/clients-body-validator.ts b/worklenz-backend/src/middlewares/validators/clients-body-validator.ts new file mode 100644 index 00000000..04c4e499 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/clients-body-validator.ts @@ -0,0 +1,15 @@ +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 {name} = req.body; + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + + req.body.name = req.body.name.trim(); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/gantt-tasks-query-params-validator.ts b/worklenz-backend/src/middlewares/validators/gantt-tasks-query-params-validator.ts new file mode 100644 index 00000000..155be429 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/gantt-tasks-query-params-validator.ts @@ -0,0 +1,12 @@ +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 {project_id} = req.query; + if (!project_id) + return res.status(200).send(new ServerResponse(false, null, "Project ID is required")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/gantt-tasks-range-params-validator.ts b/worklenz-backend/src/middlewares/validators/gantt-tasks-range-params-validator.ts new file mode 100644 index 00000000..25b20cd6 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/gantt-tasks-range-params-validator.ts @@ -0,0 +1,19 @@ +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 {project_id, start_date, end_date} = req.query; + if (!project_id) + return res.status(200).send(new ServerResponse(false, null, "Project ID is required")); + + if (!start_date) + return res.status(200).send(new ServerResponse(false, null, "Start date is required")); + + if (!end_date) + return res.status(200).send(new ServerResponse(false, null, "End date is required")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/home-task-body-validator.ts b/worklenz-backend/src/middlewares/validators/home-task-body-validator.ts new file mode 100644 index 00000000..23ffe642 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/home-task-body-validator.ts @@ -0,0 +1,24 @@ +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 {name, end_date, project_id} = req.body; + if (!name?.trim().length) + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + if (!end_date?.trim().length) + return res.status(200).send(new ServerResponse(false, null, "Due date is required")); + if (!project_id) + return res.status(200).send(new ServerResponse(false, null, "Project is required")); + + req.body.reporter_id = req.user?.id ?? null; + req.body.team_id = req.user?.team_id ?? null; + + req.body.inline = req.query.inline || false; + + if (req.body.name.length > 100) + return res.status(200).send(new ServerResponse(false, null, "Task name length exceeded!")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/id-param-validator.ts b/worklenz-backend/src/middlewares/validators/id-param-validator.ts new file mode 100644 index 00000000..462c9d54 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/id-param-validator.ts @@ -0,0 +1,11 @@ +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 { + if (!req.params.id) + return res.status(400).send(new ServerResponse(false, null)); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/import-task-templates-validator.ts b/worklenz-backend/src/middlewares/validators/import-task-templates-validator.ts new file mode 100644 index 00000000..d2ca7d39 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/import-task-templates-validator.ts @@ -0,0 +1,15 @@ +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 { + if (!req.params.id) + return res.status(400).send(new ServerResponse(false, null)); + + if (!req.body.length) + return res.status(400).send(new ServerResponse(false, null, "Tasks are required!")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/job-titles-body-validator.ts b/worklenz-backend/src/middlewares/validators/job-titles-body-validator.ts new file mode 100644 index 00000000..04c4e499 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/job-titles-body-validator.ts @@ -0,0 +1,15 @@ +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 {name} = req.body; + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + + req.body.name = req.body.name.trim(); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/kanban-status-update-validator.ts b/worklenz-backend/src/middlewares/validators/kanban-status-update-validator.ts new file mode 100644 index 00000000..bf674bae --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/kanban-status-update-validator.ts @@ -0,0 +1,13 @@ +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 {status_id, task_id} = req.params; + + if (!status_id || !task_id) + return res.status(200).send(new ServerResponse(false, null, "Updating status failed!")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/organization-owner-validator.ts b/worklenz-backend/src/middlewares/validators/organization-owner-validator.ts new file mode 100644 index 00000000..f95ae5e4 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/organization-owner-validator.ts @@ -0,0 +1,11 @@ +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 { + if (req.user?.owner) + return next(); + return res.status(401).send(new ServerResponse(false, null, "You are not authorized to perform this action")); +} diff --git a/worklenz-backend/src/middlewares/validators/organization-settings-validator.ts b/worklenz-backend/src/middlewares/validators/organization-settings-validator.ts new file mode 100644 index 00000000..805e0888 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/organization-settings-validator.ts @@ -0,0 +1,12 @@ +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 {name} = req.body; + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + return next(); +} \ No newline at end of file diff --git a/worklenz-backend/src/middlewares/validators/password-validator.ts b/worklenz-backend/src/middlewares/validators/password-validator.ts new file mode 100644 index 00000000..cef9c49f --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/password-validator.ts @@ -0,0 +1,34 @@ +import {NextFunction, Request, Response} from "express"; + +import {ServerResponse} from "../../models/server-response"; +import {isProduction} from "../../shared/utils"; +import {PasswordStrengthChecker} from "../../shared/password-strength-check"; +import {PASSWORD_POLICY} from "../../shared/constants"; + +function isStrongPassword(password: string) { + if (!isProduction()) return true; + const strength = PasswordStrengthChecker.validate(password); + return strength.value >= 2 && strength.length < 32; +} + +export default function (req: Request, res: Response, next: NextFunction) { + const {confirm_password, new_password, password} = req.body; + + const psw = (password || "").trim(); + const newPws = (new_password || "").trim(); + + if (!psw) + return res.status(200).send(new ServerResponse(false, null, "Password is required")); + + if (newPws) { + if (!isStrongPassword(newPws)) + return res.status(200).send(new ServerResponse(false, null, PASSWORD_POLICY).withTitle("Please use a strong new password.")); + + if (newPws !== confirm_password) + return res.status(200).send(new ServerResponse(false, null, "Passwords do not match")); + } else if (!isStrongPassword(psw)) { + return res.status(200).send(new ServerResponse(false, null, PASSWORD_POLICY).withTitle("Please use a strong password.")); + } + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/profile-settings-body-validator.ts b/worklenz-backend/src/middlewares/validators/profile-settings-body-validator.ts new file mode 100644 index 00000000..b58371e8 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/profile-settings-body-validator.ts @@ -0,0 +1,12 @@ +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 {name} = req.body; + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/project-manager-validator.ts b/worklenz-backend/src/middlewares/validators/project-manager-validator.ts new file mode 100644 index 00000000..1ec423c7 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/project-manager-validator.ts @@ -0,0 +1,22 @@ +import {NextFunction} from "express"; + +import {IWorkLenzRequest} from "../../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../../interfaces/worklenz-response"; +import {ServerResponse} from "../../models/server-response"; +import ProjectsController from "../../controllers/projects-controller"; + +export default async function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): Promise { + + let is_project_manager = false; + + if (req.query.current_project_id) { + const result = await ProjectsController.getProjectManager(req.query.current_project_id as string); + if (result.length) + if (req.user && (result[0].team_member_id === req.user?.team_member_id)) is_project_manager = true; + } + + if (req.user && (req.user.owner || req.user.is_admin || is_project_manager)) + return next(); + return res.status(401).send(new ServerResponse(false, null, "You are not authorized to perform this action")); +} + diff --git a/worklenz-backend/src/middlewares/validators/project-member-invite-validator.ts b/worklenz-backend/src/middlewares/validators/project-member-invite-validator.ts new file mode 100644 index 00000000..73b4d4a9 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/project-member-invite-validator.ts @@ -0,0 +1,13 @@ +import {NextFunction} from "express"; + +import {IWorkLenzRequest} from "../../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../../interfaces/worklenz-response"; +import {ServerResponse} from "../../models/server-response"; +import {isValidateEmail} from "../../shared/utils"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + const {project_id, email} = req.body; + if (!project_id || !isValidateEmail(email)) + return res.status(400).send(new ServerResponse(false, null)); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/project-member-validator.ts b/worklenz-backend/src/middlewares/validators/project-member-validator.ts new file mode 100644 index 00000000..2343b56b --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/project-member-validator.ts @@ -0,0 +1,36 @@ +import { NextFunction } from "express"; +import { IWorkLenzRequest } from "../../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../../interfaces/worklenz-response"; +import { ServerResponse } from "../../models/server-response"; +import ProjectMembersController from "../../controllers/project-members-controller"; + +export default async function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): Promise { + + const projectId = req.body.project_id; + const teamMemberId = req.user?.team_member_id; + const defaultView = req.body.default_view; + + if (!req.body.project_id || !defaultView || !teamMemberId) { + return res.status(401).send(new ServerResponse(false, null, "Unknown error has occured")); + } + + const isProjectMember = await ProjectMembersController.checkIfMemberExists(projectId, teamMemberId as string); + + if (isProjectMember) { + return next(); + } + + req.body.team_member_id = teamMemberId; + 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 isProjectMemberAssigned = await ProjectMembersController.createOrInviteMembers(req.body); + + if (isProjectMemberAssigned) { + return next(); + } + + return res.status(401).send(new ServerResponse(false, null, "Cannot assign as Project Member")); + +} diff --git a/worklenz-backend/src/middlewares/validators/projects-body-validator.ts b/worklenz-backend/src/middlewares/validators/projects-body-validator.ts new file mode 100644 index 00000000..668ed804 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/projects-body-validator.ts @@ -0,0 +1,34 @@ +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 {name, color_code, status_id} = req.body; + if (!status_id || status_id.trim() === "") + return res.status(400).send(new ServerResponse(false, null)); + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Project name is required")); + if (!color_code || color_code.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Color code is required")); + + req.body.name = req.body.name.trim(); + + if (req.body.name.length > 100) + return res.status(200).send(new ServerResponse(false, null, "Project name length exceeded!")); + + if (req.body.notes && req.body.notes.length > 200) + return res.status(200).send(new ServerResponse(false, null, "Project note length exceeded!")); + + if (req.body.working_days && !(Number.isInteger(req.body.working_days))) + return res.status(200).send(new ServerResponse(false, null, "Please use integer values")); + + if (req.body.man_days && !(Number.isInteger(req.body.man_days))) + return res.status(200).send(new ServerResponse(false, null, "Please use integer values")); + + if (req.body.hours_per_day && !(Number.isInteger(req.body.hours_per_day))) + return res.status(200).send(new ServerResponse(false, null, "Please use integer values")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/pt-task-status-body-validator.ts b/worklenz-backend/src/middlewares/validators/pt-task-status-body-validator.ts new file mode 100644 index 00000000..048a8fb3 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/pt-task-status-body-validator.ts @@ -0,0 +1,22 @@ +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 {name, template_id, category_id} = req.body; + + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + + if (!template_id || template_id.trim() === "") + return res.status(400).send(new ServerResponse(false, null)); + if (!category_id || category_id.trim() === "") + return res.status(400).send(new ServerResponse(false, null)); + + req.body.color_code = req.body.color_code || "#a9a9a9"; + req.body.default_status = !!req.body.default_status; + req.body.name = req.body.name.trim(); + + return next(); +} \ No newline at end of file diff --git a/worklenz-backend/src/middlewares/validators/quick-task-validator.ts b/worklenz-backend/src/middlewares/validators/quick-task-validator.ts new file mode 100644 index 00000000..6936f6a6 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/quick-task-validator.ts @@ -0,0 +1,15 @@ +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 {name, project_id} = req.body; + if (!name) + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + if (!project_id) + return res.status(200).send(new ServerResponse(false, null, "Project is required")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/reset-email-validator.ts b/worklenz-backend/src/middlewares/validators/reset-email-validator.ts new file mode 100644 index 00000000..b13ad7d7 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/reset-email-validator.ts @@ -0,0 +1,17 @@ +import {NextFunction} from "express"; + +import {IWorkLenzRequest} from "../../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../../interfaces/worklenz-response"; +import {ServerResponse} from "../../models/server-response"; +import {isValidateEmail} from "../../shared/utils"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + const {email} = req.body; + if (!email) + return res.status(200).send(new ServerResponse(false, null, "Email is required")); + + if (!isValidateEmail(email)) + return res.status(200).send(new ServerResponse(false, null, "Invalid email address")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/setup-validator.ts b/worklenz-backend/src/middlewares/validators/setup-validator.ts new file mode 100644 index 00000000..1e401278 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/setup-validator.ts @@ -0,0 +1,34 @@ +import { NextFunction } from "express"; + +import { IWorkLenzRequest } from "../../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../../interfaces/worklenz-response"; +import { ServerResponse } from "../../models/server-response"; +import { isValidateEmail } from "../../shared/utils"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + const { team_name, project_name, template_id } = req.body; + if (template_id && team_name) { + return next(); + } + + if (!template_id) { + if (!team_name) + return res.status(200).send(new ServerResponse(false, null, "Account name is required")); + + if (!project_name) + return res.status(200).send(new ServerResponse(false, null, "Project name is required")); + + req.body.tasks = Array.isArray(req.body.tasks) ? req.body.tasks : []; + req.body.team_members = Array.isArray(req.body.team_members) ? req.body.team_members.filter((i: string) => !!i) : []; + + if (!req.body.tasks.length) + return res.status(200).send(new ServerResponse(false, null, "At least one task is required")); + + for (const email of req.body.team_members) { + if (email && !isValidateEmail(email)) + return res.status(200).send(new ServerResponse(false, null, "One or more of your team members has invalid email addresses. Please double check and try again.").withTitle("Account setup failed!")); + } + } + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/shared-projects-create-validator.ts b/worklenz-backend/src/middlewares/validators/shared-projects-create-validator.ts new file mode 100644 index 00000000..28493ca7 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/shared-projects-create-validator.ts @@ -0,0 +1,12 @@ +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 {project_id} = req.body; + if (!project_id) + return res.status(400).send(new ServerResponse(false, null)); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/sign-up-validator.ts b/worklenz-backend/src/middlewares/validators/sign-up-validator.ts new file mode 100644 index 00000000..5c00df6c --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/sign-up-validator.ts @@ -0,0 +1,13 @@ +import {NextFunction, Request, Response} from "express"; + +import {ServerResponse} from "../../models/server-response"; +import {isValidateEmail} from "../../shared/utils"; + +export default function (req: Request, res: Response, next: NextFunction) { + const {name, email} = req.body; + if (!name) return res.status(200).send(new ServerResponse(false, null, "Name is required")); + if (!email) return res.status(200).send(new ServerResponse(false, null, "Email is required")); + if (!isValidateEmail(email)) return res.status(200).send(new ServerResponse(false, null, "Invalid email address")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/status-delete-validator.ts b/worklenz-backend/src/middlewares/validators/status-delete-validator.ts new file mode 100644 index 00000000..df5fc377 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/status-delete-validator.ts @@ -0,0 +1,15 @@ +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 {project, replace} = req.query; + if (!project) + return res.status(400).send(new ServerResponse(false, null)); + + req.query.replace = /null/.test(replace as string) ? null : replace as any; + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/status-order-validator.ts b/worklenz-backend/src/middlewares/validators/status-order-validator.ts new file mode 100644 index 00000000..1609fefb --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/status-order-validator.ts @@ -0,0 +1,11 @@ +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 { + if (!req.body.status_order) + return res.status(200).send(new ServerResponse(false, null, "Invalid request. Please try again. ")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/task-attachments-validator.ts b/worklenz-backend/src/middlewares/validators/task-attachments-validator.ts new file mode 100644 index 00000000..b8f67ad9 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/task-attachments-validator.ts @@ -0,0 +1,21 @@ +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 {file, file_name, project_id, size} = req.body; + + if (!file || !file_name || !project_id || !size) + return res.status(200).send(new ServerResponse(false, null, "Upload failed")); + + if (size > 5.243e+7) + return res.status(200).send(new ServerResponse(false, null, "Max file size for attachments is 50 MB.").withTitle("Upload failed!")); + + req.body.type = file_name.split(".").pop(); + + req.body.task_id = req.body.task_id || null; + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/task-comment-body-validator.ts b/worklenz-backend/src/middlewares/validators/task-comment-body-validator.ts new file mode 100644 index 00000000..7742cfe3 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/task-comment-body-validator.ts @@ -0,0 +1,21 @@ +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 {content, task_id} = req.body; + if (!content) + return res.status(200).send(new ServerResponse(false, null, "Comment message is required")); + if (!task_id) + return res.status(200).send(new ServerResponse(false, null, "Unable to create comment")); + if (content.length > 2000) + return res.status(200).send(new ServerResponse(false, null, "Message length exceeded")); + + req.body.mentions = Array.isArray(req.body.mentions) + ? req.body.mentions + : []; + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/task-phase-name-validator.ts b/worklenz-backend/src/middlewares/validators/task-phase-name-validator.ts new file mode 100644 index 00000000..0dd095b0 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/task-phase-name-validator.ts @@ -0,0 +1,12 @@ +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 name = (req.body.name || "").trim(); + if (!name) + return res.status(400).send(new ServerResponse(false, null, "Invalid name")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/task-status-body-validator.ts b/worklenz-backend/src/middlewares/validators/task-status-body-validator.ts new file mode 100644 index 00000000..a77cb3cb --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/task-status-body-validator.ts @@ -0,0 +1,22 @@ +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 {name, project_id, category_id} = req.body; + + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + + if (!project_id || project_id.trim() === "") + return res.status(400).send(new ServerResponse(false, null)); + if (!category_id || category_id.trim() === "") + return res.status(400).send(new ServerResponse(false, null)); + + req.body.color_code = req.body.color_code || "#a9a9a9"; + req.body.default_status = !!req.body.default_status; + req.body.name = req.body.name.trim(); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/task-time-log-validator.ts b/worklenz-backend/src/middlewares/validators/task-time-log-validator.ts new file mode 100644 index 00000000..cf77dad8 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/task-time-log-validator.ts @@ -0,0 +1,13 @@ +import { NextFunction } from "express"; + +import { IWorkLenzRequest } from "../../interfaces/worklenz-request"; +import { IWorkLenzResponse } from "../../interfaces/worklenz-response"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + + const {id, seconds_spent, created_at, formatted_start} = req.body; + + if (!id || !seconds_spent || !formatted_start) return res.sendStatus(400); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/tasks-body-validator.ts b/worklenz-backend/src/middlewares/validators/tasks-body-validator.ts new file mode 100644 index 00000000..47b24657 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/tasks-body-validator.ts @@ -0,0 +1,51 @@ +import {NextFunction} from "express"; + +import {IWorkLenzRequest} from "../../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../../interfaces/worklenz-response"; +import {ServerResponse} from "../../models/server-response"; +import {getRandomColorCode, sanitize, toMinutes, toRound} from "../../shared/utils"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + const {name, priority_id, status_id, assignees, project_id, labels} = req.body; + if (!name?.trim()?.length) + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + if (!priority_id) + return res.status(200).send(new ServerResponse(false, null, "Priority is required")); + if (!status_id) + return res.status(200).send(new ServerResponse(false, null, "Status is required")); + if (!project_id) + return res.status(200).send(new ServerResponse(false, null, "Project is required")); + + req.body.total_hours = isNaN(+req.body.total_hours) || req.body.total_hours > 1000 ? 0 : toRound(req.body.total_hours); + req.body.total_minutes = isNaN(+req.body.total_minutes) || req.body.total_minutes > 1000 ? 0 : toRound(req.body.total_minutes); + + req.body.assignees = Array.isArray(assignees) ? assignees : []; + req.body.labels = Array.isArray(labels) ? labels : []; + + req.body.reporter_id = req.user?.id || null; + req.body.total_minutes = toMinutes(req.body.total_hours, req.body.total_minutes); + req.body.team_id = req.user?.team_id || null; + + req.body.inline = req.query.inline || false; + + const labelsJson = []; + for (const label of req.body.labels) { + labelsJson.push({ + name: label, + color: getRandomColorCode() + }); + } + + req.body.labels = labelsJson; + + if (req.body.description) { + if (req.body.description.length > 4000) + return res.status(200).send(new ServerResponse(false, null, "Task description length exceeded!")); + req.body.description = sanitize(req.body.description); + } + + if (req.body.name.length > 100) + return res.status(200).send(new ServerResponse(false, null, "Task name length exceeded!")); + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/team-members-body-validator.ts b/worklenz-backend/src/middlewares/validators/team-members-body-validator.ts new file mode 100644 index 00000000..7ed6f9d0 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/team-members-body-validator.ts @@ -0,0 +1,20 @@ +import {NextFunction} from "express"; + +import {IWorkLenzRequest} from "../../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../../interfaces/worklenz-response"; +import {ServerResponse} from "../../models/server-response"; +import {isValidateEmail} from "../../shared/utils"; + +export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void { + const {emails} = req.body; + + if (!Array.isArray(emails) || !emails.length) + return res.status(200).send(new ServerResponse(false, null, "Email addresses cannot be empty")); + + for (const email of emails) { + if (!isValidateEmail(email)) + return res.status(200).send(new ServerResponse(false, null, "Invalid email address")); + } + + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/team-owner-or-admin-validator.ts b/worklenz-backend/src/middlewares/validators/team-owner-or-admin-validator.ts new file mode 100644 index 00000000..108adff2 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/team-owner-or-admin-validator.ts @@ -0,0 +1,11 @@ +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 { + if (req.user && (req.user.owner || req.user.is_admin)) + return next(); + return res.status(401).send(new ServerResponse(false, null, "You are not authorized to perform this action")); +} diff --git a/worklenz-backend/src/middlewares/validators/team-settings-body-validator.ts b/worklenz-backend/src/middlewares/validators/team-settings-body-validator.ts new file mode 100644 index 00000000..b58371e8 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/team-settings-body-validator.ts @@ -0,0 +1,12 @@ +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 {name} = req.body; + if (!name || name.trim() === "") + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/teams-activate-body-validator.ts b/worklenz-backend/src/middlewares/validators/teams-activate-body-validator.ts new file mode 100644 index 00000000..2a9e7120 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/teams-activate-body-validator.ts @@ -0,0 +1,12 @@ +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 {id} = req.body; + if (!id) + return res.status(200).send(new ServerResponse(false, null, "Invalid team identifier")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/todo-list-body-validator.ts b/worklenz-backend/src/middlewares/validators/todo-list-body-validator.ts new file mode 100644 index 00000000..74c27911 --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/todo-list-body-validator.ts @@ -0,0 +1,16 @@ +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 {name, color_code} = req.body; + if (!name) + return res.status(200).send(new ServerResponse(false, null, "Name is required")); + if (!color_code) + req.body.color_code = "#767676"; + + req.body.done = !!req.body.done; + return next(); +} diff --git a/worklenz-backend/src/middlewares/validators/update-password-validator.ts b/worklenz-backend/src/middlewares/validators/update-password-validator.ts new file mode 100644 index 00000000..3cb9937a --- /dev/null +++ b/worklenz-backend/src/middlewares/validators/update-password-validator.ts @@ -0,0 +1,15 @@ +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 {hash, password, user} = req.body; + if (!password) + return res.status(200).send(new ServerResponse(false, null, "Password is required")); + + if (!hash || !user) + return res.status(200).send(new ServerResponse(false, null, "An unknown error has occurred. Please try again.")); + return next(); +} diff --git a/worklenz-backend/src/middlewares/verify-project-membership.ts b/worklenz-backend/src/middlewares/verify-project-membership.ts new file mode 100644 index 00000000..e2bdfbfc --- /dev/null +++ b/worklenz-backend/src/middlewares/verify-project-membership.ts @@ -0,0 +1,28 @@ +import {NextFunction} from "express"; +import {IWorkLenzRequest} from "../interfaces/worklenz-request"; +import {IWorkLenzResponse} from "../interfaces/worklenz-response"; +import createHttpError from "http-errors"; +import db from "../config/db"; + +export default function (projectId: string) { + return async (req: IWorkLenzRequest, _res: IWorkLenzResponse, next: NextFunction) => { + const userId = req.user?.id; + const teamId = req.user?.team_id; + + try { + const q = ` + SELECT 1 + FROM project_members + WHERE project_id = $1 + AND team_member_id = (SELECT id FROM team_members WHERE team_id = $2 AND user_id = $3); + `; + const result = await db.query(q, [projectId, teamId, userId]); + if (result.rowCount) + return next(); + } catch (error) { + // ignored + } + + return next(createHttpError(401)); + }; +} diff --git a/worklenz-backend/src/migrations/.gitkeep b/worklenz-backend/src/migrations/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/worklenz-backend/src/migrations/add-projects-keys.ts b/worklenz-backend/src/migrations/add-projects-keys.ts new file mode 100644 index 00000000..ba7db69b --- /dev/null +++ b/worklenz-backend/src/migrations/add-projects-keys.ts @@ -0,0 +1,53 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const {Client} = require("pg"); + +import dotenv from "dotenv"; +import {generateProjectKey} from "../utils/generate-project-key"; + +dotenv.config(); + +const client = new Client({ + 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, + max: process.env.DB_MAX_CLIENTS, + idleTimeoutMillis: 30000, +}); + +function log(message: string) { + console.log("Projects Keys Migration:", message); +} + +async function getAllKeysByTeamId(teamId: string) { + if (!teamId) return []; + try { + const result = await client.query("SELECT key FROM projects WHERE team_id = $1 ORDER BY key;", [teamId]); + return result.rows.map((project: any) => project.key).filter((key: any) => !!key); + } catch (error) { + return []; + } +} + +async function runProjectsKeyMigration() { + log("migration started"); + const result = await client.query("SELECT id, name, team_id FROM projects WHERE key IS NULL ORDER BY created_at;", []); + log(`${result.rows.length} projects found`); + for (const project of result.rows) { + const keys = await getAllKeysByTeamId(project.team_id); + const key = generateProjectKey(project.name, keys); + await client.query("UPDATE projects SET key = $1 WHERE id = $2", [key, project.id]); + log(`${project.name} (${key})`); + } + log("migration ended"); +} + +client.connect(async (error: any) => { + if (!error) { + await runProjectsKeyMigration(); + client.end(); + } else { + console.error(error); + } +}); diff --git a/worklenz-backend/src/models/.gitkeep b/worklenz-backend/src/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/worklenz-backend/src/models/auth-response.ts b/worklenz-backend/src/models/auth-response.ts new file mode 100644 index 00000000..639e73b3 --- /dev/null +++ b/worklenz-backend/src/models/auth-response.ts @@ -0,0 +1,17 @@ +import {IPassportSession} from "../interfaces/passport-session"; + +export class AuthResponse { + private authenticated = false; + private user: IPassportSession | null = null; + private title: string | null = null; + private auth_error: string | null = null; + private message: string | null = null; + + constructor(title: string | null, authenticated: boolean, user: IPassportSession | null, auth_error: string | null, message: string | null) { + this.title = title; + this.authenticated = !!authenticated; + this.user = user; + this.auth_error = auth_error; + this.message = message; + } +} diff --git a/worklenz-backend/src/models/reporting-export.ts b/worklenz-backend/src/models/reporting-export.ts new file mode 100644 index 00000000..f829d094 --- /dev/null +++ b/worklenz-backend/src/models/reporting-export.ts @@ -0,0 +1,166 @@ +import moment from "moment"; +import db from "../config/db"; +import ReportingOverviewBase from "../controllers/reporting/overview/reporting-overview-base"; +import { formatDuration, getColor, int } from "../shared/utils"; + +export class ReportingExportModel extends ReportingOverviewBase { + + + public static async getMembersByTeam(teamId: string | null) { + 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) AS projects, + (SELECT COUNT(*) + FROM tasks_assignees + WHERE tasks_assignees.team_member_id = team_member_info_view.team_member_id) 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) 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))) 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))) AS ongoing + FROM team_member_info_view + WHERE team_id = $1 + ORDER BY name; + `; + + const result = await db.query(q, [teamId]); + return result.rows; + } + + + public static async getProjectMembers(projectId: string) { + const q = ` + SELECT pm.id, + pm.team_member_id, + (SELECT name FROM team_member_info_view WHERE team_member_id = pm.team_member_id) AS name, + COUNT(ta.task_id) AS tasks_count, + COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS TRUE THEN 1 END) AS completed, + COUNT(CASE WHEN is_completed(t.status_id, t.project_id) IS FALSE THEN 1 END) AS incompleted, + COUNT(CASE WHEN is_overdue(t.id) THEN 1 END) AS overdue, + (SELECT SUM(time_spent) + FROM task_work_log twl + WHERE user_id = + (SELECT user_id FROM team_member_info_view WHERE team_member_id = pm.team_member_id) + AND twl.task_id IN (SELECT id FROM tasks WHERE project_id = pm.project_id)) AS time_logged + FROM project_members pm + LEFT JOIN tasks_assignees ta + ON pm.id = ta.project_member_id AND ta.team_member_id = pm.team_member_id + LEFT JOIN tasks t ON ta.task_id = t.id + WHERE pm.project_id = $1 + GROUP BY pm.id + ORDER BY name + `; + const result = await db.query(q, [projectId]); + + const total = await this.getTotalTasksCount(projectId); + + for (const member of result.rows) { + member.tasks_count = int(member.tasks_count); + member.completed = int(member.completed); + member.incompleted = int(member.incompleted); + member.overdue = int(member.overdue); + member.contribution = this.getPercentage(member.tasks_count, total); + member.progress = this.getPercentage(member.completed, member.tasks_count); + member.time_logged = formatDuration(moment.duration(int(member.time_logged) || "0", "seconds")); + } + + return result.rows; + + } + + + public static async getMemberTasks(teamMemberId: string, projectId: string | null, onlySingleMember: string, key: string, dateRange: string[] | [], includeArchived: boolean, userId: string) { + + const projectFilter = projectId ? ` AND t.project_id = $2` : ""; + + let activityLogDurationFilterClause = ``; + let archivedClause = ``; + if (onlySingleMember === "true") { + activityLogDurationFilterClause = this.activityLogDurationFilter(key, dateRange); + 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 = ` + WITH work_log AS (SELECT task_id, SUM(time_spent) AS total_time_spent + FROM task_work_log + GROUP BY task_id) + 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 projects WHERE id = t.project_id) AS project_name, + (SELECT name + FROM teams + WHERE id = (SELECT team_id FROM projects WHERE projects.id = t.project_id)) AS team_name, + (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, + tp.name AS priority_name, + tp.color_code AS priority_color, + ts.name AS status_name, + sc.color_code AS status_color, + t.end_date, + t.completed_at, + + (total_minutes * 60) AS total_minutes, + (work_log.total_time_spent - (total_minutes * 60)) AS overlogged_time, + + (SELECT SUM(time_spent) + FROM task_work_log twl + WHERE team_member_id = ta.team_member_id + AND twl.task_id = t.id + AND twl.task_id IN + (SELECT id FROM tasks WHERE tasks.project_id = t.project_id)) AS time_logged + FROM tasks t + JOIN tasks_assignees ta ON t.id = ta.task_id + LEFT JOIN work_log ON work_log.task_id = t.id + LEFT JOIN task_priorities tp ON t.priority_id = tp.id + LEFT JOIN task_statuses ts ON t.status_id = ts.id + LEFT JOIN sys_task_status_categories sc ON ts.category_id = sc.id + WHERE ta.team_member_id = $1 ${projectFilter} ${archivedClause} + ORDER BY t.end_date DESC;`; + + const params = projectId ? [teamMemberId, projectId] : [teamMemberId]; + const result = await db.query(q, params); + + for (const project of result.rows) { + project.project_color = getColor(project.project_name); + // estimated time + project.estimated_string = formatDuration(moment.duration(~~(project.total_minutes), "seconds")); + // logged time + project.time_spent_string = formatDuration(moment.duration(~~(project.time_logged), "seconds")); + // overlogged_time + project.overlogged_time = formatDuration(moment.duration(~~(project.overlogged_time), "seconds")); + + project.completed_date = project.completed_at ? project.completed_at : null; + } + + return result.rows; + + } + + +} diff --git a/worklenz-backend/src/models/server-response.ts b/worklenz-backend/src/models/server-response.ts new file mode 100644 index 00000000..86c49e73 --- /dev/null +++ b/worklenz-backend/src/models/server-response.ts @@ -0,0 +1,22 @@ +export class ServerResponse { + public done: boolean; + public body: T | null; + public title: string | null = null; + public message: string | null; + + constructor(done: boolean, body: T, message: string | null = null) { + this.done = !!done; + this.body = body === null || body === undefined ? null : body; + this.message = message?.toString().trim() ?? null; + } + + public withTitle(title: string) { + this.title = title; + return this; + } + + public setMessage(message: string) { + this.message = message; + return this; + } +} diff --git a/worklenz-backend/src/passport/deserialize.ts b/worklenz-backend/src/passport/deserialize.ts new file mode 100644 index 00000000..ccb6509c --- /dev/null +++ b/worklenz-backend/src/passport/deserialize.ts @@ -0,0 +1,42 @@ +import moment from "moment"; +import db from "../config/db"; +import {IDeserializeCallback} from "../interfaces/deserialize-callback"; +import {IPassportSession} from "../interfaces/passport-session"; + +async function setLastActive(id: string) { + try { + await db.query("UPDATE users SET last_active = CURRENT_TIMESTAMP WHERE id = $1;", [id]); + } catch (error) { + // ignored + } +} + +async function clearEmailInvitations(email: string, teamId: string) { + try { + await db.query("DELETE FROM email_invitations WHERE email = $1 AND team_id = $2;", [email, teamId]); + } catch (error) { + // ignored + } +} + +// Check whether the user still exists on the database +export async function deserialize(id: string, done: IDeserializeCallback) { + try { + const q = `SELECT deserialize_user($1) AS user;`; + const result = await db.query(q, [id]); + if (result.rows.length) { + const [data] = result.rows; + if (data?.user) { + data.user.is_member = !!data.user.team_member_id; + + void setLastActive(data.user.id); + void clearEmailInvitations(data.user.email, data.user.team_id); + + return done(null, data.user as IPassportSession); + } + } + return done(null, null); + } catch (error) { + return done(error, null); + } +} diff --git a/worklenz-backend/src/passport/index.ts b/worklenz-backend/src/passport/index.ts new file mode 100644 index 00000000..6c3575ae --- /dev/null +++ b/worklenz-backend/src/passport/index.ts @@ -0,0 +1,20 @@ +import {PassportStatic} from "passport"; + +import {deserialize} from "./deserialize"; +import {serialize} from "./serialize"; + +import GoogleLogin from "./passport-strategies/passport-google"; +import LocalLogin from "./passport-strategies/passport-local-login"; +import LocalSignup from "./passport-strategies/passport-local-signup"; + +/** + * Use any passport middleware before the serialize and deserialize + * @param {Passport} passport + */ +export default (passport: PassportStatic) => { + passport.use("local-login", LocalLogin); + passport.use("local-signup", LocalSignup); + passport.use(GoogleLogin); + passport.serializeUser(serialize); + passport.deserializeUser(deserialize); +}; diff --git a/worklenz-backend/src/passport/passport-strategies/passport-constants.ts b/worklenz-backend/src/passport/passport-strategies/passport-constants.ts new file mode 100644 index 00000000..59080ff7 --- /dev/null +++ b/worklenz-backend/src/passport/passport-strategies/passport-constants.ts @@ -0,0 +1,2 @@ +export const SUCCESS_KEY = "success"; +export const ERROR_KEY = "error"; diff --git a/worklenz-backend/src/passport/passport-strategies/passport-google.ts b/worklenz-backend/src/passport/passport-strategies/passport-google.ts new file mode 100644 index 00000000..04cbccd4 --- /dev/null +++ b/worklenz-backend/src/passport/passport-strategies/passport-google.ts @@ -0,0 +1,73 @@ +import GoogleStrategy from "passport-google-oauth20"; +import {sendWelcomeEmail} from "../../shared/email-templates"; +import {log_error} from "../../shared/utils"; +import db from "../../config/db"; +import {ERROR_KEY} from "./passport-constants"; +import {Request} from "express"; + +async function handleGoogleLogin(req: Request, _accessToken: string, _refreshToken: string, profile: GoogleStrategy.Profile, done: GoogleStrategy.VerifyCallback) { + try { + const body: any = profile; + if (Array.isArray(profile.emails) && profile.emails.length) body.email = profile.emails[0].value; + if (Array.isArray(profile.photos) && profile.photos.length) body.picture = profile.photos[0].value; + + // Check for existing accounts signed up using OAuth + const localAccountResult = await db.query("SELECT 1 FROM users WHERE email = $1 AND password IS NOT NULL;", [body.email]); + if (localAccountResult.rowCount) { + const message = `No Google account exists for email ${body.email}.`; + (req.session as any).error = message; + return done(null, undefined, req.flash(ERROR_KEY, message)); + } + + // If the user came from an invitation, this exists + const state = JSON.parse(req.query.state as string || "{}"); + if (state) { + body.team = state.team; + body.member_id = state.teamMember; + } + + const q1 = `SELECT id, google_id, name, email, active_team + FROM users + WHERE google_id = $1 + OR email = $2;`; + const result1 = await db.query(q1, [body.id, body.email]); + + if (result1.rowCount) { // Login + const [user] = result1.rows; + + // Update active team of users who came from an invitation + try { + await db.query("SELECT set_active_team($1, $2);", [user.id || null, state.team || null]); + } catch (error) { + log_error(error, user); + } + + if (user) + return done(null, user); + + } else { // Register + const q2 = `SELECT register_google_user($1) AS user;`; + const result2 = await db.query(q2, [JSON.stringify(body)]); + const [data] = result2.rows; + + sendWelcomeEmail(data.user.email, body.displayName); + return done(null, data.user, {message: "User successfully logged in"}); + } + + return done(null); + } catch (error: any) { + return done(error); + } +} + +/** + * Passport strategy for authenticate with google + * http://www.passportjs.org/packages/passport-google-oauth20/ + */ +export default new GoogleStrategy.Strategy({ + clientID: process.env.GOOGLE_CLIENT_ID as string, + clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, + callbackURL: process.env.GOOGLE_CALLBACK_URL as string, + passReqToCallback: true + }, + (req, _accessToken, _refreshToken, profile, done) => void handleGoogleLogin(req, _accessToken, _refreshToken, profile, done)); diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts new file mode 100644 index 00000000..67f74052 --- /dev/null +++ b/worklenz-backend/src/passport/passport-strategies/passport-local-login.ts @@ -0,0 +1,46 @@ +import bcrypt from "bcrypt"; +import {Strategy as LocalStrategy} from "passport-local"; + +import {log_error} from "../../shared/utils"; +import db from "../../config/db"; +import {Request} from "express"; + +async function handleLogin(req: Request, email: string, password: string, done: any) { + (req.session as any).flash = {}; + + if (!email || !password) + return done(null, false, {message: "Invalid credentials."}); + + try { + // select the user from the database based on the username + const q = `SELECT id, email, google_id, password + FROM users + WHERE email = $1 + AND google_id IS NULL;`; + const result = await db.query(q, [email]); + const [data] = result.rows; + + // Check user existence + if (!data?.password) + return done(null, false, {message: "Invalid credentials."}); + + // Compare the password & email + if (bcrypt.compareSync(password, data.password) && email === data.email) { + delete data.password; + + req.logout(() => true); + return done(false, data, {message: "User successfully logged in"}); + } + + return done(null, false, {message: "Invalid credentials."}); + } catch (error) { + log_error(error, req.body); + return done(error); + } +} + +export default new LocalStrategy({ + usernameField: "email", // = email + passwordField: "password", + passReqToCallback: true +}, (req, email, password, done) => void handleLogin(req, email, password, done)); diff --git a/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts b/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts new file mode 100644 index 00000000..cfc66562 --- /dev/null +++ b/worklenz-backend/src/passport/passport-strategies/passport-local-signup.ts @@ -0,0 +1,96 @@ +import bcrypt from "bcrypt"; +import {Strategy as LocalStrategy} from "passport-local"; + +import {DEFAULT_ERROR_MESSAGE} from "../../shared/constants"; +import {sendWelcomeEmail} from "../../shared/email-templates"; +import {log_error} from "../../shared/utils"; + +import db from "../../config/db"; +import {Request} from "express"; +import {ERROR_KEY, SUCCESS_KEY} from "./passport-constants"; + +async function isGoogleAccountFound(email: string) { + const q = ` + SELECT 1 + FROM users + WHERE email = $1 + AND google_id IS NOT NULL; + `; + const result = await db.query(q, [email]); + return !!result.rowCount; +} + +async function registerUser(password: string, team_id: string, name: string, team_name: string, email: string, timezone: string, team_member_id: string) { + const salt = bcrypt.genSaltSync(10); + const encryptedPassword = bcrypt.hashSync(password, salt); + + const teamId = team_id || null; + const q = "SELECT register_user($1) AS user;"; + + const body = { + name, + team_name, + email, + password: encryptedPassword, + timezone, + invited_team_id: teamId, + team_member_id, + }; + + const result = await db.query(q, [JSON.stringify(body)]); + const [data] = result.rows; + return data.user; +} + +async function handleSignUp(req: Request, email: string, password: string, done: any) { + (req.session as any).flash = {}; + // team = Invited team_id if req.body.from_invitation is true + const {name, team_name, team_member_id, team_id, timezone} = req.body; + + if (!team_name) return done(null, null, req.flash(ERROR_KEY, "Team name is required")); + + const googleAccountFound = await isGoogleAccountFound(email); + if (googleAccountFound) + return done(null, null, req.flash(ERROR_KEY, `${req.body.email} is already linked with a Google account.`)); + + try { + const user = await registerUser(password, team_id, name, team_name, email, timezone, team_member_id); + sendWelcomeEmail(email, name); + + setTimeout(() => { + return done(null, user, req.flash(SUCCESS_KEY, "Registration successful. Please check your email for verification.")); + }, 500); + + } catch (error: any) { + const message = (error?.message) || ""; + + if (message === "ERROR_INVALID_JOINING_EMAIL") { + return done(null, null, req.flash(ERROR_KEY, `No invitations found for email ${req.body.email}.`)); + } + + // if error.message is "email already exists" then it should have the email address in the error message after ":". + if (message.includes("EMAIL_EXISTS_ERROR") || error.constraint === "users_google_id_uindex") { + const [, value] = error.message.split(":"); + return done(null, null, req.flash(ERROR_KEY, `Worklenz account already exists for email ${value}.`)); + } + + if (message.includes("TEAM_NAME_EXISTS_ERROR")) { + const [, value] = error.message.split(":"); + return done(null, null, req.flash(ERROR_KEY, `Team name "${value}" already exists. Please choose a different team name.`)); + } + + // The Team name is already taken. + if (error.constraint === "teams_url_uindex" || error.constraint === "teams_name_uindex") { + return done(null, null, req.flash(ERROR_KEY, `Team name "${team_name}" is already taken. Please choose a different team name.`)); + } + + log_error(error, req.body); + return done(null, null, req.flash(ERROR_KEY, DEFAULT_ERROR_MESSAGE)); + } +} + +export default new LocalStrategy({ + usernameField: "email", + passwordField: "password", + passReqToCallback: true +}, (req, email, password, done) => void handleSignUp(req, email, password, done)); diff --git a/worklenz-backend/src/passport/serialize.ts b/worklenz-backend/src/passport/serialize.ts new file mode 100644 index 00000000..ae7ed7d5 --- /dev/null +++ b/worklenz-backend/src/passport/serialize.ts @@ -0,0 +1,7 @@ +import {ISerializeCallback} from "../interfaces/serialize-callback"; +import {IPassportSession} from "../interfaces/passport-session"; + +// Parse the user id to deserialize function +export function serialize($user: IPassportSession, done: ISerializeCallback) { + done(null, $user?.id ?? null); +} diff --git a/worklenz-backend/src/pg_notify_listeners/db-task-status-changed.ts b/worklenz-backend/src/pg_notify_listeners/db-task-status-changed.ts new file mode 100644 index 00000000..477db2d9 --- /dev/null +++ b/worklenz-backend/src/pg_notify_listeners/db-task-status-changed.ts @@ -0,0 +1,111 @@ +import {Client, Notification} from "pg"; +import dbConfig from "../config/db-config"; +import db from "../config/db"; +import {ITaskMovedToDoneRecord} from "../interfaces/task-moved-to-done"; +import {sendTaskDone} from "../shared/email-notifications"; +import {getBaseUrl} from "../cron_jobs/helpers"; + +export default class DbTaskStatusChangeListener { + private static connected = false; + + public static async connect() { + try { + + const client = new Client(dbConfig); + + await client.connect(); + + await client.query("UNLISTEN db_task_status_changed"); + await client.query("LISTEN db_task_status_changed"); + + client.on("notification", (notification: Notification) => { + if (notification.channel === "db_task_status_changed") { + const taskId = notification.payload; + if (taskId) { + void this.sendEmails(taskId); + } + } + }); + + client.on("error", (err: Error) => { + this.error(err); + }); + + console.info("DbTaskStatusChangeListener connected."); + } catch (err: any) { + this.error(err); + } + } + + public static disconnect() { + if (!this.connected) return; + console.info("DbTaskStatusChangeListener disconnected."); + } + + private static error(err: Error) { + this.connected = false; + console.error("DbTaskStatusChangeListener disconnected with errors.", err); + } + + private static async sendEmails(taskId: string) { + const q = ` + WITH subscribers AS ( + -- + SELECT t.name AS task_name, + u.name AS user_name, + ts.task_id, + u.email, + (SELECT project_id FROM tasks WHERE id = ts.task_id) AS project_id, + (SELECT name + FROM teams + WHERE id = (SELECT team_id FROM team_members WHERE id = ts.team_member_id)) AS team_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 = ts.task_id) AS members + FROM task_subscribers ts + LEFT JOIN users u ON u.id = ts.user_id + LEFT JOIN tasks t ON t.id = ts.task_id + WHERE ts.task_id = $1 + ORDER BY ts.created_at + -- + ) + SELECT task_name, + user_name, + task_id, + email, + project_id, + team_name, + members, + (SELECT name FROM projects WHERE id = project_id) AS project_name + FROM subscribers; + `; + + const result = await db.query(q, [taskId]); + + for (const data of result.rows) { + const taskUrl = `${getBaseUrl()}/worklenz/projects/${data.project_id}?tab=tasks-list&task=${data.task_id}`; + const settingsUrl = `${getBaseUrl()}/worklenz/settings/notifications`; + + const task = { + name: data.task_name, + members: data.members, + url: taskUrl, + team_name: data.team_name, + project_name: data.project_name + }; + + const payload: ITaskMovedToDoneRecord = { + greeting: `Hi ${data.user_name}`, + summary: "Great news! a task just got completed!", + settings_url: settingsUrl, + task + }; + + void sendTaskDone([data.email], payload); + } + } +} diff --git a/worklenz-backend/src/public/140.090f8f66847cac61.js b/worklenz-backend/src/public/140.090f8f66847cac61.js new file mode 100644 index 00000000..f424c3f4 --- /dev/null +++ b/worklenz-backend/src/public/140.090f8f66847cac61.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkworklenz=self.webpackChunkworklenz||[]).push([[140],{99183:(_e,q,h)=>{h.d(q,{o:()=>$});var _=h(96814),l=h(96928),r=h(96109),S=h(2919),e=h(33640),g=h(65879);function Y(p,M){1&p&&g.GkF(0)}const Q=function(p){return[p,24]},J=function(){return{background:"#52c41a",border:"4px solid #52c41a"}},L=function(p){return{$implicit:p}};function Z(p,M){if(1&p&&(g.TgZ(0,"nz-badge",4),g.YNc(1,Y,1,0,"ng-container",5),g.qZA()),2&p){const z=M.$implicit;g.oxw(2);const m=g.MAs(5);g.Q6J("nzOffset",g.VKq(4,Q,-4))("nzStyle",g.DdM(6,J)),g.xp6(1),g.Q6J("ngTemplateOutlet",m)("ngTemplateOutletContext",g.VKq(7,L,z))}}const W=function(){return[]};function V(p,M){if(1&p&&(g.ynx(0),g.YNc(1,Z,2,9,"nz-badge",3),g.BQk()),2&p){const z=g.oxw();g.xp6(1),g.Q6J("ngForOf",z.names||g.DdM(1,W))}}function U(p,M){1&p&&g.GkF(0)}function ee(p,M){if(1&p&&(g.ynx(0),g.YNc(1,U,1,0,"ng-container",5),g.BQk()),2&p){const z=M.$implicit;g.oxw(2);const m=g.MAs(5);g.xp6(1),g.Q6J("ngTemplateOutlet",m)("ngTemplateOutletContext",g.VKq(2,L,z))}}function P(p,M){if(1&p&&(g.ynx(0),g.YNc(1,ee,2,4,"ng-container",6),g.BQk()),2&p){const z=g.oxw();g.xp6(1),g.Q6J("ngForOf",z.names||g.DdM(1,W))}}function k(p,M){if(1&p&&(g._UZ(0,"nz-avatar",7),g.ALo(1,"firstCharUpper")),2&p){const z=M.$implicit,m=g.oxw();g.Tol(m.avatarClass),g.Udp("background-color",z.avatar_url?"#ececec":z.color_code),g.Q6J("nzSize",28)("nzText",z.end?z.name:g.lcZ(1,9,z.name))("nzSrc",z.avatar_url)("nzTooltipTitle",z.end&&z.names?z.names:z.name)("nzTooltipPlacement","top")}}let $=(()=>{var p;class M{constructor(){this.names=[],this.avatarClass=null,this.showDot=!1}}return(p=M).\u0275fac=function(m){return new(m||p)},p.\u0275cmp=g.Xpm({type:p,selectors:[["worklenz-avatars"]],inputs:{names:"names",avatarClass:"avatarClass",showDot:"showDot"},standalone:!0,features:[g.jDz],decls:6,vars:3,consts:[[3,"ngSwitch"],[4,"ngSwitchCase"],["templateRef",""],["nzDot","",3,"nzOffset","nzStyle",4,"ngFor","ngForOf"],["nzDot","",3,"nzOffset","nzStyle"],[4,"ngTemplateOutlet","ngTemplateOutletContext"],[4,"ngFor","ngForOf"],["nz-tooltip","",3,"nzSize","nzText","nzSrc","nzTooltipTitle","nzTooltipPlacement"]],template:function(m,A){1&m&&(g.TgZ(0,"nz-avatar-group"),g.ynx(1,0),g.YNc(2,V,2,2,"ng-container",1),g.YNc(3,P,2,2,"ng-container",1),g.BQk(),g.qZA(),g.YNc(4,k,2,11,"ng-template",null,2,g.W1O)),2&m&&(g.xp6(1),g.Q6J("ngSwitch",A.showDot),g.xp6(1),g.Q6J("ngSwitchCase",!0),g.xp6(1),g.Q6J("ngSwitchCase",!1))},dependencies:[l.Rt,l.Dz,l.ZM,r.cg,r.SY,_.sg,S.I,e.mS,e.x7,_.RF,_.tP,_.n9],changeDetection:0}),M})()},2919:(_e,q,h)=>{h.d(q,{I:()=>l});var _=h(65879);let l=(()=>{var r;class S{transform(g,...Y){return g?g.charAt(0).toUpperCase():""}}return(r=S).\u0275fac=function(g){return new(g||r)},r.\u0275pipe=_.Yjl({name:"firstCharUpper",type:r,pure:!0,standalone:!0}),S})()},82962:(_e,q,h)=>{h.d(q,{bd:()=>b,l7:()=>E,vh:()=>F});var _=h(97582),l=h(65879),r=h(27754),S=h(78645),e=h(59773),g=h(40874),Y=h(49388),Q=h(96814),J=h(8324);function L(d,x){1&d&&l.Hsn(0)}const Z=["*"];function W(d,x){1&d&&(l.TgZ(0,"div",4),l._UZ(1,"div",5),l.qZA()),2&d&&l.Q6J("ngClass",x.$implicit)}function V(d,x){if(1&d&&(l.TgZ(0,"div",2),l.YNc(1,W,2,1,"div",3),l.qZA()),2&d){const C=x.$implicit;l.xp6(1),l.Q6J("ngForOf",C)}}function U(d,x){if(1&d&&(l.ynx(0),l._uU(1),l.BQk()),2&d){const C=l.oxw(3);l.xp6(1),l.Oqu(C.nzTitle)}}function ee(d,x){if(1&d&&(l.TgZ(0,"div",11),l.YNc(1,U,2,1,"ng-container",12),l.qZA()),2&d){const C=l.oxw(2);l.xp6(1),l.Q6J("nzStringTemplateOutlet",C.nzTitle)}}function P(d,x){if(1&d&&(l.ynx(0),l._uU(1),l.BQk()),2&d){const C=l.oxw(3);l.xp6(1),l.Oqu(C.nzExtra)}}function k(d,x){if(1&d&&(l.TgZ(0,"div",13),l.YNc(1,P,2,1,"ng-container",12),l.qZA()),2&d){const C=l.oxw(2);l.xp6(1),l.Q6J("nzStringTemplateOutlet",C.nzExtra)}}function $(d,x){}function p(d,x){if(1&d&&(l.ynx(0),l.YNc(1,$,0,0,"ng-template",14),l.BQk()),2&d){const C=l.oxw(2);l.xp6(1),l.Q6J("ngTemplateOutlet",C.listOfNzCardTabComponent.template)}}function M(d,x){if(1&d&&(l.TgZ(0,"div",6)(1,"div",7),l.YNc(2,ee,2,1,"div",8),l.YNc(3,k,2,1,"div",9),l.qZA(),l.YNc(4,p,2,1,"ng-container",10),l.qZA()),2&d){const C=l.oxw();l.xp6(2),l.Q6J("ngIf",C.nzTitle),l.xp6(1),l.Q6J("ngIf",C.nzExtra),l.xp6(1),l.Q6J("ngIf",C.listOfNzCardTabComponent)}}function z(d,x){}function m(d,x){if(1&d&&(l.TgZ(0,"div",15),l.YNc(1,z,0,0,"ng-template",14),l.qZA()),2&d){const C=l.oxw();l.xp6(1),l.Q6J("ngTemplateOutlet",C.nzCover)}}function A(d,x){1&d&&(l.ynx(0),l.Hsn(1),l.BQk())}function f(d,x){1&d&&l._UZ(0,"nz-card-loading")}function v(d,x){}function H(d,x){if(1&d&&(l.TgZ(0,"li")(1,"span"),l.YNc(2,v,0,0,"ng-template",14),l.qZA()()),2&d){const C=x.$implicit,D=l.oxw(2);l.Udp("width",100/D.nzActions.length,"%"),l.xp6(2),l.Q6J("ngTemplateOutlet",C)}}function ae(d,x){if(1&d&&(l.TgZ(0,"ul",16),l.YNc(1,H,3,3,"li",17),l.qZA()),2&d){const C=l.oxw();l.xp6(1),l.Q6J("ngForOf",C.nzActions)}}function oe(d,x){}function le(d,x){if(1&d&&(l.TgZ(0,"div",2),l.YNc(1,oe,0,0,"ng-template",3),l.qZA()),2&d){const C=l.oxw();l.xp6(1),l.Q6J("ngTemplateOutlet",C.nzAvatar)}}function re(d,x){if(1&d&&(l.ynx(0),l._uU(1),l.BQk()),2&d){const C=l.oxw(3);l.xp6(1),l.Oqu(C.nzTitle)}}function ce(d,x){if(1&d&&(l.TgZ(0,"div",7),l.YNc(1,re,2,1,"ng-container",8),l.qZA()),2&d){const C=l.oxw(2);l.xp6(1),l.Q6J("nzStringTemplateOutlet",C.nzTitle)}}function fe(d,x){if(1&d&&(l.ynx(0),l._uU(1),l.BQk()),2&d){const C=l.oxw(3);l.xp6(1),l.Oqu(C.nzDescription)}}function ze(d,x){if(1&d&&(l.TgZ(0,"div",9),l.YNc(1,fe,2,1,"ng-container",8),l.qZA()),2&d){const C=l.oxw(2);l.xp6(1),l.Q6J("nzStringTemplateOutlet",C.nzDescription)}}function me(d,x){if(1&d&&(l.TgZ(0,"div",4),l.YNc(1,ce,2,1,"div",5),l.YNc(2,ze,2,1,"div",6),l.qZA()),2&d){const C=l.oxw();l.xp6(1),l.Q6J("ngIf",C.nzTitle),l.xp6(1),l.Q6J("ngIf",C.nzDescription)}}let Ce=(()=>{var d;class x{constructor(){this.nzHoverable=!0}}return(d=x).\u0275fac=function(D){return new(D||d)},d.\u0275dir=l.lG2({type:d,selectors:[["","nz-card-grid",""]],hostAttrs:[1,"ant-card-grid"],hostVars:2,hostBindings:function(D,I){2&D&&l.ekj("ant-card-hoverable",I.nzHoverable)},inputs:{nzHoverable:"nzHoverable"},exportAs:["nzCardGrid"]}),(0,_.gn)([(0,r.yF)()],x.prototype,"nzHoverable",void 0),x})(),xe=(()=>{var d;class x{}return(d=x).\u0275fac=function(D){return new(D||d)},d.\u0275cmp=l.Xpm({type:d,selectors:[["nz-card-tab"]],viewQuery:function(D,I){if(1&D&&l.Gf(l.Rgc,7),2&D){let te;l.iGM(te=l.CRH())&&(I.template=te.first)}},exportAs:["nzCardTab"],ngContentSelectors:Z,decls:1,vars:0,template:function(D,I){1&D&&(l.F$t(),l.YNc(0,L,1,0,"ng-template"))},encapsulation:2,changeDetection:0}),x})(),O=(()=>{var d;class x{constructor(){this.listOfLoading=[["ant-col-22"],["ant-col-8","ant-col-15"],["ant-col-6","ant-col-18"],["ant-col-13","ant-col-9"],["ant-col-4","ant-col-3","ant-col-16"],["ant-col-8","ant-col-6","ant-col-8"]]}}return(d=x).\u0275fac=function(D){return new(D||d)},d.\u0275cmp=l.Xpm({type:d,selectors:[["nz-card-loading"]],hostAttrs:[1,"ant-card-loading-content"],exportAs:["nzCardLoading"],decls:2,vars:1,consts:[[1,"ant-card-loading-content"],["class","ant-row","style","margin-left: -4px; margin-right: -4px;",4,"ngFor","ngForOf"],[1,"ant-row",2,"margin-left","-4px","margin-right","-4px"],["style","padding-left: 4px; padding-right: 4px;",3,"ngClass",4,"ngFor","ngForOf"],[2,"padding-left","4px","padding-right","4px",3,"ngClass"],[1,"ant-card-loading-block"]],template:function(D,I){1&D&&(l.TgZ(0,"div",0),l.YNc(1,V,2,1,"div",1),l.qZA()),2&D&&(l.xp6(1),l.Q6J("ngForOf",I.listOfLoading))},dependencies:[Q.mk,Q.sg],encapsulation:2,changeDetection:0}),x})();const R="card";let b=(()=>{var d;class x{constructor(D,I,te){this.nzConfigService=D,this.cdr=I,this.directionality=te,this._nzModuleName=R,this.nzBordered=!0,this.nzBorderless=!1,this.nzLoading=!1,this.nzHoverable=!1,this.nzBodyStyle=null,this.nzActions=[],this.nzType=null,this.nzSize="default",this.dir="ltr",this.destroy$=new S.x,this.nzConfigService.getConfigChangeEventForComponent(R).pipe((0,e.R)(this.destroy$)).subscribe(()=>{this.cdr.markForCheck()})}ngOnInit(){this.directionality.change?.pipe((0,e.R)(this.destroy$)).subscribe(D=>{this.dir=D,this.cdr.detectChanges()}),this.dir=this.directionality.value}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}}return(d=x).\u0275fac=function(D){return new(D||d)(l.Y36(g.jY),l.Y36(l.sBO),l.Y36(Y.Is,8))},d.\u0275cmp=l.Xpm({type:d,selectors:[["nz-card"]],contentQueries:function(D,I,te){if(1&D&&(l.Suo(te,xe,5),l.Suo(te,Ce,4)),2&D){let he;l.iGM(he=l.CRH())&&(I.listOfNzCardTabComponent=he.first),l.iGM(he=l.CRH())&&(I.listOfNzCardGridDirective=he)}},hostAttrs:[1,"ant-card"],hostVars:16,hostBindings:function(D,I){2&D&&l.ekj("ant-card-loading",I.nzLoading)("ant-card-bordered",!1===I.nzBorderless&&I.nzBordered)("ant-card-hoverable",I.nzHoverable)("ant-card-small","small"===I.nzSize)("ant-card-contain-grid",I.listOfNzCardGridDirective&&I.listOfNzCardGridDirective.length)("ant-card-type-inner","inner"===I.nzType)("ant-card-contain-tabs",!!I.listOfNzCardTabComponent)("ant-card-rtl","rtl"===I.dir)},inputs:{nzBordered:"nzBordered",nzBorderless:"nzBorderless",nzLoading:"nzLoading",nzHoverable:"nzHoverable",nzBodyStyle:"nzBodyStyle",nzCover:"nzCover",nzActions:"nzActions",nzType:"nzType",nzSize:"nzSize",nzTitle:"nzTitle",nzExtra:"nzExtra"},exportAs:["nzCard"],ngContentSelectors:Z,decls:7,vars:6,consts:[["class","ant-card-head",4,"ngIf"],["class","ant-card-cover",4,"ngIf"],[1,"ant-card-body",3,"ngStyle"],[4,"ngIf","ngIfElse"],["loadingTemplate",""],["class","ant-card-actions",4,"ngIf"],[1,"ant-card-head"],[1,"ant-card-head-wrapper"],["class","ant-card-head-title",4,"ngIf"],["class","ant-card-extra",4,"ngIf"],[4,"ngIf"],[1,"ant-card-head-title"],[4,"nzStringTemplateOutlet"],[1,"ant-card-extra"],[3,"ngTemplateOutlet"],[1,"ant-card-cover"],[1,"ant-card-actions"],[3,"width",4,"ngFor","ngForOf"]],template:function(D,I){if(1&D&&(l.F$t(),l.YNc(0,M,5,3,"div",0),l.YNc(1,m,2,1,"div",1),l.TgZ(2,"div",2),l.YNc(3,A,2,0,"ng-container",3),l.YNc(4,f,1,0,"ng-template",null,4,l.W1O),l.qZA(),l.YNc(6,ae,2,1,"ul",5)),2&D){const te=l.MAs(5);l.Q6J("ngIf",I.nzTitle||I.nzExtra||I.listOfNzCardTabComponent),l.xp6(1),l.Q6J("ngIf",I.nzCover),l.xp6(1),l.Q6J("ngStyle",I.nzBodyStyle),l.xp6(1),l.Q6J("ngIf",!I.nzLoading)("ngIfElse",te),l.xp6(3),l.Q6J("ngIf",I.nzActions.length)}},dependencies:[Q.sg,Q.O5,Q.tP,Q.PC,J.f,O],encapsulation:2,changeDetection:0}),(0,_.gn)([(0,g.oS)(),(0,r.yF)()],x.prototype,"nzBordered",void 0),(0,_.gn)([(0,g.oS)(),(0,r.yF)()],x.prototype,"nzBorderless",void 0),(0,_.gn)([(0,r.yF)()],x.prototype,"nzLoading",void 0),(0,_.gn)([(0,g.oS)(),(0,r.yF)()],x.prototype,"nzHoverable",void 0),(0,_.gn)([(0,g.oS)()],x.prototype,"nzSize",void 0),x})(),E=(()=>{var d;class x{constructor(){this.nzTitle=null,this.nzDescription=null,this.nzAvatar=null}}return(d=x).\u0275fac=function(D){return new(D||d)},d.\u0275cmp=l.Xpm({type:d,selectors:[["nz-card-meta"]],hostAttrs:[1,"ant-card-meta"],inputs:{nzTitle:"nzTitle",nzDescription:"nzDescription",nzAvatar:"nzAvatar"},exportAs:["nzCardMeta"],decls:2,vars:2,consts:[["class","ant-card-meta-avatar",4,"ngIf"],["class","ant-card-meta-detail",4,"ngIf"],[1,"ant-card-meta-avatar"],[3,"ngTemplateOutlet"],[1,"ant-card-meta-detail"],["class","ant-card-meta-title",4,"ngIf"],["class","ant-card-meta-description",4,"ngIf"],[1,"ant-card-meta-title"],[4,"nzStringTemplateOutlet"],[1,"ant-card-meta-description"]],template:function(D,I){1&D&&(l.YNc(0,le,2,1,"div",0),l.YNc(1,me,3,2,"div",1)),2&D&&(l.Q6J("ngIf",I.nzAvatar),l.xp6(1),l.Q6J("ngIf",I.nzTitle||I.nzDescription))},dependencies:[Q.O5,Q.tP,J.f],encapsulation:2,changeDetection:0}),x})(),F=(()=>{var d;class x{}return(d=x).\u0275fac=function(D){return new(D||d)},d.\u0275mod=l.oAB({type:d}),d.\u0275inj=l.cJS({imports:[Q.ez,J.T,Y.vT]}),x})()},19035:(_e,q,h)=>{h.d(q,{JW:()=>me,_p:()=>xe});var _=h(97582),l=h(96814),r=h(65879),S=h(78645),e=h(59773),g=h(81374),Y=h(64716),Q=h(64194),J=h(40874),L=h(27754),Z=h(96109),W=h(331),V=h(49388),U=h(42840),ee=h(70855),P=h(41958),k=h(69594),$=h(62595),p=h(8324),M=h(85448),z=h(4300),m=h(64345);const A=["okBtn"],f=["cancelBtn"];function v(O,R){1&O&&(r.TgZ(0,"div",15),r._UZ(1,"span",16),r.qZA())}function H(O,R){if(1&O&&(r.ynx(0),r._UZ(1,"span",18),r.BQk()),2&O){const b=R.$implicit;r.xp6(1),r.Q6J("nzType",b||"exclamation-circle")}}function ae(O,R){if(1&O&&(r.ynx(0),r.YNc(1,H,2,1,"ng-container",8),r.TgZ(2,"div",17),r._uU(3),r.qZA(),r.BQk()),2&O){const b=r.oxw(2);r.xp6(1),r.Q6J("nzStringTemplateOutlet",b.nzIcon),r.xp6(2),r.Oqu(b.nzTitle)}}function oe(O,R){if(1&O&&(r.ynx(0),r._uU(1),r.BQk()),2&O){const b=r.oxw(2);r.xp6(1),r.Oqu(b.nzCancelText)}}function le(O,R){1&O&&(r.ynx(0),r._uU(1),r.ALo(2,"nzI18n"),r.BQk()),2&O&&(r.xp6(1),r.Oqu(r.lcZ(2,1,"Modal.cancelText")))}function re(O,R){if(1&O&&(r.ynx(0),r._uU(1),r.BQk()),2&O){const b=r.oxw(2);r.xp6(1),r.Oqu(b.nzOkText)}}function ce(O,R){1&O&&(r.ynx(0),r._uU(1),r.ALo(2,"nzI18n"),r.BQk()),2&O&&(r.xp6(1),r.Oqu(r.lcZ(2,1,"Modal.okText")))}function fe(O,R){if(1&O){const b=r.EpF();r.TgZ(0,"div",2)(1,"div",3),r.YNc(2,v,2,0,"div",4),r.TgZ(3,"div",5)(4,"div")(5,"div",6)(6,"div",7),r.YNc(7,ae,4,2,"ng-container",8),r.qZA(),r.TgZ(8,"div",9)(9,"button",10,11),r.NdJ("click",function(){r.CHM(b);const F=r.oxw();return r.KtG(F.onCancel())}),r.YNc(11,oe,2,1,"ng-container",12),r.YNc(12,le,3,3,"ng-container",12),r.qZA(),r.TgZ(13,"button",13,14),r.NdJ("click",function(){r.CHM(b);const F=r.oxw();return r.KtG(F.onConfirm())}),r.YNc(15,re,2,1,"ng-container",12),r.YNc(16,ce,3,3,"ng-container",12),r.qZA()()()()()()()}if(2&O){const b=r.oxw();r.ekj("ant-popover-rtl","rtl"===b.dir),r.Q6J("cdkTrapFocusAutoCapture",null!==b.nzAutoFocus)("ngClass",b._classMap)("ngStyle",b.nzOverlayStyle)("@.disabled",!(null==b.noAnimation||!b.noAnimation.nzNoAnimation))("nzNoAnimation",null==b.noAnimation?null:b.noAnimation.nzNoAnimation)("@zoomBigMotion","active"),r.xp6(2),r.Q6J("ngIf",b.nzPopconfirmShowArrow),r.xp6(5),r.Q6J("nzStringTemplateOutlet",b.nzTitle),r.xp6(2),r.Q6J("nzSize","small"),r.uIk("cdkFocusInitial","cancel"===b.nzAutoFocus||null),r.xp6(2),r.Q6J("ngIf",b.nzCancelText),r.xp6(1),r.Q6J("ngIf",!b.nzCancelText),r.xp6(1),r.Q6J("nzSize","small")("nzType","danger"!==b.nzOkType?b.nzOkType:"primary")("nzDanger",b.nzOkDanger||"danger"===b.nzOkType)("nzLoading",b.confirmLoading),r.uIk("cdkFocusInitial","ok"===b.nzAutoFocus||null),r.xp6(2),r.Q6J("ngIf",b.nzOkText),r.xp6(1),r.Q6J("ngIf",!b.nzOkText)}}let me=(()=>{var O;class R extends Z.Mg{getProxyPropertyMap(){return{nzOkText:["nzOkText",()=>this.nzOkText],nzOkType:["nzOkType",()=>this.nzOkType],nzOkDanger:["nzOkDanger",()=>this.nzOkDanger],nzCancelText:["nzCancelText",()=>this.nzCancelText],nzBeforeConfirm:["nzBeforeConfirm",()=>this.nzBeforeConfirm],nzCondition:["nzCondition",()=>this.nzCondition],nzIcon:["nzIcon",()=>this.nzIcon],nzPopconfirmShowArrow:["nzPopconfirmShowArrow",()=>this.nzPopconfirmShowArrow],nzPopconfirmBackdrop:["nzBackdrop",()=>this.nzPopconfirmBackdrop],nzAutoFocus:["nzAutoFocus",()=>this.nzAutofocus],...super.getProxyPropertyMap()}}constructor(E,F,d,x,C,D){super(E,F,d,x,C,D),this._nzModuleName="popconfirm",this.trigger="click",this.placement="top",this.nzCondition=!1,this.nzPopconfirmShowArrow=!0,this.nzPopconfirmBackdrop=!1,this.nzAutofocus=null,this.visibleChange=new r.vpe,this.nzOnCancel=new r.vpe,this.nzOnConfirm=new r.vpe,this.componentRef=this.hostView.createComponent(Ce)}createComponent(){super.createComponent(),this.component.nzOnCancel.pipe((0,e.R)(this.destroy$)).subscribe(()=>{this.nzOnCancel.emit()}),this.component.nzOnConfirm.pipe((0,e.R)(this.destroy$)).subscribe(()=>{this.nzOnConfirm.emit()})}}return(O=R).\u0275fac=function(E){return new(E||O)(r.Y36(r.SBq),r.Y36(r.s_b),r.Y36(r._Vd),r.Y36(r.Qsj),r.Y36(W.P,9),r.Y36(J.jY))},O.\u0275dir=r.lG2({type:O,selectors:[["","nz-popconfirm",""]],hostVars:2,hostBindings:function(E,F){2&E&&r.ekj("ant-popover-open",F.visible)},inputs:{arrowPointAtCenter:["nzPopconfirmArrowPointAtCenter","arrowPointAtCenter"],title:["nzPopconfirmTitle","title"],directiveTitle:["nz-popconfirm","directiveTitle"],trigger:["nzPopconfirmTrigger","trigger"],placement:["nzPopconfirmPlacement","placement"],origin:["nzPopconfirmOrigin","origin"],mouseEnterDelay:["nzPopconfirmMouseEnterDelay","mouseEnterDelay"],mouseLeaveDelay:["nzPopconfirmMouseLeaveDelay","mouseLeaveDelay"],overlayClassName:["nzPopconfirmOverlayClassName","overlayClassName"],overlayStyle:["nzPopconfirmOverlayStyle","overlayStyle"],visible:["nzPopconfirmVisible","visible"],nzOkText:"nzOkText",nzOkType:"nzOkType",nzOkDanger:"nzOkDanger",nzCancelText:"nzCancelText",nzBeforeConfirm:"nzBeforeConfirm",nzIcon:"nzIcon",nzCondition:"nzCondition",nzPopconfirmShowArrow:"nzPopconfirmShowArrow",nzPopconfirmBackdrop:"nzPopconfirmBackdrop",nzAutofocus:"nzAutofocus"},outputs:{visibleChange:"nzPopconfirmVisibleChange",nzOnCancel:"nzOnCancel",nzOnConfirm:"nzOnConfirm"},exportAs:["nzPopconfirm"],features:[r.qOj]}),(0,_.gn)([(0,L.yF)()],R.prototype,"arrowPointAtCenter",void 0),(0,_.gn)([(0,L.yF)()],R.prototype,"nzOkDanger",void 0),(0,_.gn)([(0,L.yF)()],R.prototype,"nzCondition",void 0),(0,_.gn)([(0,L.yF)()],R.prototype,"nzPopconfirmShowArrow",void 0),(0,_.gn)([(0,J.oS)()],R.prototype,"nzPopconfirmBackdrop",void 0),(0,_.gn)([(0,J.oS)()],R.prototype,"nzAutofocus",void 0),R})(),Ce=(()=>{var O;class R extends Z.XK{constructor(E,F,d,x,C){super(E,d,C),this.elementRef=F,this.nzCondition=!1,this.nzPopconfirmShowArrow=!0,this.nzOkType="primary",this.nzOkDanger=!1,this.nzAutoFocus=null,this.nzBeforeConfirm=null,this.nzOnCancel=new S.x,this.nzOnConfirm=new S.x,this._trigger="click",this.elementFocusedBeforeModalWasOpened=null,this._prefix="ant-popover",this.confirmLoading=!1,this.document=x}ngOnDestroy(){super.ngOnDestroy(),this.nzOnCancel.complete(),this.nzOnConfirm.complete()}show(){this.nzCondition?this.onConfirm():(this.capturePreviouslyFocusedElement(),super.show())}hide(){super.hide(),this.restoreFocus()}handleConfirm(){this.nzOnConfirm.next(),super.hide()}onCancel(){this.nzOnCancel.next(),super.hide()}onConfirm(){if(this.nzBeforeConfirm){const E=(0,L.lN)(this.nzBeforeConfirm()).pipe((0,g.P)());this.confirmLoading=!0,E.pipe((0,Y.x)(()=>{this.confirmLoading=!1,this.cdr.markForCheck()}),(0,e.R)(this.nzVisibleChange),(0,e.R)(this.destroy$)).subscribe(F=>{F&&this.handleConfirm()})}else this.handleConfirm()}capturePreviouslyFocusedElement(){this.document&&(this.elementFocusedBeforeModalWasOpened=this.document.activeElement)}restoreFocus(){const E=this.elementFocusedBeforeModalWasOpened;if(E&&"function"==typeof E.focus){const F=this.document.activeElement,d=this.elementRef.nativeElement;(!F||F===this.document.body||F===d||d.contains(F))&&E.focus()}}}return(O=R).\u0275fac=function(E){return new(E||O)(r.Y36(r.sBO),r.Y36(r.SBq),r.Y36(V.Is,8),r.Y36(l.K0,8),r.Y36(W.P,9))},O.\u0275cmp=r.Xpm({type:O,selectors:[["nz-popconfirm"]],viewQuery:function(E,F){if(1&E&&(r.Gf(A,5,r.SBq),r.Gf(f,5,r.SBq)),2&E){let d;r.iGM(d=r.CRH())&&(F.okBtn=d),r.iGM(d=r.CRH())&&(F.cancelBtn=d)}},exportAs:["nzPopconfirmComponent"],features:[r.qOj],decls:2,vars:6,consts:[["cdkConnectedOverlay","","nzConnectedOverlay","",3,"cdkConnectedOverlayHasBackdrop","cdkConnectedOverlayOrigin","cdkConnectedOverlayPositions","cdkConnectedOverlayOpen","cdkConnectedOverlayPush","nzArrowPointAtCenter","overlayOutsideClick","detach","positionChange"],["overlay","cdkConnectedOverlay"],["cdkTrapFocus","",1,"ant-popover",3,"cdkTrapFocusAutoCapture","ngClass","ngStyle","nzNoAnimation"],[1,"ant-popover-content"],["class","ant-popover-arrow",4,"ngIf"],[1,"ant-popover-inner"],[1,"ant-popover-inner-content"],[1,"ant-popover-message"],[4,"nzStringTemplateOutlet"],[1,"ant-popover-buttons"],["nz-button","",3,"nzSize","click"],["cancelBtn",""],[4,"ngIf"],["nz-button","",3,"nzSize","nzType","nzDanger","nzLoading","click"],["okBtn",""],[1,"ant-popover-arrow"],[1,"ant-popover-arrow-content"],[1,"ant-popover-message-title"],["nz-icon","","nzTheme","fill",3,"nzType"]],template:function(E,F){1&E&&(r.YNc(0,fe,17,21,"ng-template",0,1,r.W1O),r.NdJ("overlayOutsideClick",function(x){return F.onClickOutside(x)})("detach",function(){return F.hide()})("positionChange",function(x){return F.onPositionChange(x)})),2&E&&r.Q6J("cdkConnectedOverlayHasBackdrop",F.nzBackdrop)("cdkConnectedOverlayOrigin",F.origin)("cdkConnectedOverlayPositions",F._positions)("cdkConnectedOverlayOpen",F._visible)("cdkConnectedOverlayPush",!0)("nzArrowPointAtCenter",F.nzArrowPointAtCenter)},dependencies:[l.mk,l.O5,l.PC,U.ix,ee.w,P.dQ,k.pI,$.Ls,p.f,M.hQ,W.P,z.mK,m.o9],encapsulation:2,data:{animation:[Q.$C]},changeDetection:0}),R})(),xe=(()=>{var O;class R{}return(O=R).\u0275fac=function(E){return new(E||O)},O.\u0275mod=r.oAB({type:O}),O.\u0275inj=r.cJS({imports:[V.vT,l.ez,U.sL,k.U8,m.YI,$.PV,p.T,M.e4,W.g,Z.cg,z.rt]}),R})()},57907:(_e,q,h)=>{h.d(q,{Bq:()=>P,Dg:()=>$,Of:()=>p,aF:()=>M});var _=h(65879),l=h(97582),r=h(60095),S=h(97328),e=h(78645),g=h(92438),Y=h(59773),Q=h(27754),J=h(49388),L=h(4300),Z=h(50883),W=h(96814);const V=["*"],U=["inputElement"],ee=["nz-radio",""];let P=(()=>{var z;class m{}return(z=m).\u0275fac=function(f){return new(f||z)},z.\u0275dir=_.lG2({type:z,selectors:[["","nz-radio-button",""]]}),m})(),k=(()=>{var z;class m{constructor(){this.selected$=new S.t(1),this.touched$=new e.x,this.disabled$=new S.t(1),this.name$=new S.t(1)}touch(){this.touched$.next()}select(f){this.selected$.next(f)}setDisabled(f){this.disabled$.next(f)}setName(f){this.name$.next(f)}}return(z=m).\u0275fac=function(f){return new(f||z)},z.\u0275prov=_.Yz7({token:z,factory:z.\u0275fac}),m})(),$=(()=>{var z;class m{constructor(f,v,H){this.cdr=f,this.nzRadioService=v,this.directionality=H,this.value=null,this.destroy$=new e.x,this.isNzDisableFirstChange=!0,this.onChange=()=>{},this.onTouched=()=>{},this.nzDisabled=!1,this.nzButtonStyle="outline",this.nzSize="default",this.nzName=null,this.dir="ltr"}ngOnInit(){this.nzRadioService.selected$.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{this.value!==f&&(this.value=f,this.onChange(this.value))}),this.nzRadioService.touched$.pipe((0,Y.R)(this.destroy$)).subscribe(()=>{Promise.resolve().then(()=>this.onTouched())}),this.directionality.change?.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{this.dir=f,this.cdr.detectChanges()}),this.dir=this.directionality.value}ngOnChanges(f){const{nzDisabled:v,nzName:H}=f;v&&this.nzRadioService.setDisabled(this.nzDisabled),H&&this.nzRadioService.setName(this.nzName)}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}writeValue(f){this.value=f,this.nzRadioService.select(f),this.cdr.markForCheck()}registerOnChange(f){this.onChange=f}registerOnTouched(f){this.onTouched=f}setDisabledState(f){this.nzDisabled=this.isNzDisableFirstChange&&this.nzDisabled||f,this.isNzDisableFirstChange=!1,this.nzRadioService.setDisabled(this.nzDisabled),this.cdr.markForCheck()}}return(z=m).\u0275fac=function(f){return new(f||z)(_.Y36(_.sBO),_.Y36(k),_.Y36(J.Is,8))},z.\u0275cmp=_.Xpm({type:z,selectors:[["nz-radio-group"]],hostAttrs:[1,"ant-radio-group"],hostVars:8,hostBindings:function(f,v){2&f&&_.ekj("ant-radio-group-large","large"===v.nzSize)("ant-radio-group-small","small"===v.nzSize)("ant-radio-group-solid","solid"===v.nzButtonStyle)("ant-radio-group-rtl","rtl"===v.dir)},inputs:{nzDisabled:"nzDisabled",nzButtonStyle:"nzButtonStyle",nzSize:"nzSize",nzName:"nzName"},exportAs:["nzRadioGroup"],features:[_._Bn([k,{provide:r.JU,useExisting:(0,_.Gpc)(()=>z),multi:!0}]),_.TTD],ngContentSelectors:V,decls:1,vars:0,template:function(f,v){1&f&&(_.F$t(),_.Hsn(0))},encapsulation:2,changeDetection:0}),(0,l.gn)([(0,Q.yF)()],m.prototype,"nzDisabled",void 0),m})(),p=(()=>{var z;class m{focus(){this.focusMonitor.focusVia(this.inputElement,"keyboard")}blur(){this.inputElement.nativeElement.blur()}constructor(f,v,H,ae,oe,le,re,ce){this.ngZone=f,this.elementRef=v,this.cdr=H,this.focusMonitor=ae,this.directionality=oe,this.nzRadioService=le,this.nzRadioButtonDirective=re,this.nzFormStatusService=ce,this.isNgModel=!1,this.destroy$=new e.x,this.isNzDisableFirstChange=!0,this.isChecked=!1,this.name=null,this.isRadioButton=!!this.nzRadioButtonDirective,this.onChange=()=>{},this.onTouched=()=>{},this.nzValue=null,this.nzDisabled=!1,this.nzAutoFocus=!1,this.dir="ltr"}setDisabledState(f){this.nzDisabled=this.isNzDisableFirstChange&&this.nzDisabled||f,this.isNzDisableFirstChange=!1,this.cdr.markForCheck()}writeValue(f){this.isChecked=f,this.cdr.markForCheck()}registerOnChange(f){this.isNgModel=!0,this.onChange=f}registerOnTouched(f){this.onTouched=f}ngOnInit(){this.nzRadioService&&(this.nzRadioService.name$.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{this.name=f,this.cdr.markForCheck()}),this.nzRadioService.disabled$.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{this.nzDisabled=this.isNzDisableFirstChange&&this.nzDisabled||f,this.isNzDisableFirstChange=!1,this.cdr.markForCheck()}),this.nzRadioService.selected$.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{const v=this.isChecked;this.isChecked=this.nzValue===f,this.isNgModel&&v!==this.isChecked&&!1===this.isChecked&&this.onChange(!1),this.cdr.markForCheck()})),this.focusMonitor.monitor(this.elementRef,!0).pipe((0,Y.R)(this.destroy$)).subscribe(f=>{f||(Promise.resolve().then(()=>this.onTouched()),this.nzRadioService&&this.nzRadioService.touch())}),this.directionality.change.pipe((0,Y.R)(this.destroy$)).subscribe(f=>{this.dir=f,this.cdr.detectChanges()}),this.dir=this.directionality.value,this.setupClickListener()}ngAfterViewInit(){this.nzAutoFocus&&this.focus()}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete(),this.focusMonitor.stopMonitoring(this.elementRef)}setupClickListener(){this.ngZone.runOutsideAngular(()=>{(0,g.R)(this.elementRef.nativeElement,"click").pipe((0,Y.R)(this.destroy$)).subscribe(f=>{f.stopPropagation(),f.preventDefault(),!this.nzDisabled&&!this.isChecked&&this.ngZone.run(()=>{this.focus(),this.nzRadioService?.select(this.nzValue),this.isNgModel&&(this.isChecked=!0,this.onChange(!0)),this.cdr.markForCheck()})})})}}return(z=m).\u0275fac=function(f){return new(f||z)(_.Y36(_.R0b),_.Y36(_.SBq),_.Y36(_.sBO),_.Y36(L.tE),_.Y36(J.Is,8),_.Y36(k,8),_.Y36(P,8),_.Y36(Z.kH,8))},z.\u0275cmp=_.Xpm({type:z,selectors:[["","nz-radio",""],["","nz-radio-button",""]],viewQuery:function(f,v){if(1&f&&_.Gf(U,7),2&f){let H;_.iGM(H=_.CRH())&&(v.inputElement=H.first)}},hostVars:18,hostBindings:function(f,v){2&f&&_.ekj("ant-radio-wrapper-in-form-item",!!v.nzFormStatusService)("ant-radio-wrapper",!v.isRadioButton)("ant-radio-button-wrapper",v.isRadioButton)("ant-radio-wrapper-checked",v.isChecked&&!v.isRadioButton)("ant-radio-button-wrapper-checked",v.isChecked&&v.isRadioButton)("ant-radio-wrapper-disabled",v.nzDisabled&&!v.isRadioButton)("ant-radio-button-wrapper-disabled",v.nzDisabled&&v.isRadioButton)("ant-radio-wrapper-rtl",!v.isRadioButton&&"rtl"===v.dir)("ant-radio-button-wrapper-rtl",v.isRadioButton&&"rtl"===v.dir)},inputs:{nzValue:"nzValue",nzDisabled:"nzDisabled",nzAutoFocus:"nzAutoFocus"},exportAs:["nzRadio"],features:[_._Bn([{provide:r.JU,useExisting:(0,_.Gpc)(()=>z),multi:!0}])],attrs:ee,ngContentSelectors:V,decls:6,vars:24,consts:[["type","radio",3,"disabled","checked"],["inputElement",""]],template:function(f,v){1&f&&(_.F$t(),_.TgZ(0,"span"),_._UZ(1,"input",0,1)(3,"span"),_.qZA(),_.TgZ(4,"span"),_.Hsn(5),_.qZA()),2&f&&(_.ekj("ant-radio",!v.isRadioButton)("ant-radio-checked",v.isChecked&&!v.isRadioButton)("ant-radio-disabled",v.nzDisabled&&!v.isRadioButton)("ant-radio-button",v.isRadioButton)("ant-radio-button-checked",v.isChecked&&v.isRadioButton)("ant-radio-button-disabled",v.nzDisabled&&v.isRadioButton),_.xp6(1),_.ekj("ant-radio-input",!v.isRadioButton)("ant-radio-button-input",v.isRadioButton),_.Q6J("disabled",v.nzDisabled)("checked",v.isChecked),_.uIk("autofocus",v.nzAutoFocus?"autofocus":null)("name",v.name),_.xp6(2),_.ekj("ant-radio-inner",!v.isRadioButton)("ant-radio-button-inner",v.isRadioButton))},encapsulation:2,changeDetection:0}),(0,l.gn)([(0,Q.yF)()],m.prototype,"nzDisabled",void 0),(0,l.gn)([(0,Q.yF)()],m.prototype,"nzAutoFocus",void 0),m})(),M=(()=>{var z;class m{}return(z=m).\u0275fac=function(f){return new(f||z)},z.\u0275mod=_.oAB({type:z}),z.\u0275inj=_.cJS({imports:[J.vT,W.ez,r.u5]}),m})()},13740:(_e,q,h)=>{h.d(q,{UX:()=>fn,qn:()=>Ee,Ql:()=>Ne,Uo:()=>_n,N8:()=>Sn,HQ:()=>yn,p0:()=>$e,qD:()=>Re,_C:()=>Be,Om:()=>Tn,$Z:()=>Je});var _=h(49388),l=h(62831),r=h(70532),S=h(96814),e=h(65879),g=h(60095),Y=h(42840),Q=h(28802),J=h(62612),L=h(8324),Z=h(62787),W=h(60804),V=h(64345),U=h(62595),ee=h(73460),P=h(97582),k=h(78645),$=h(97328),p=h(59773),M=h(40874),z=h(99087),m=h(27754),A=h(9691);const f=["nz-pagination-item",""];function v(i,a){if(1&i&&(e.TgZ(0,"a"),e._uU(1),e.qZA()),2&i){const s=e.oxw().page;e.xp6(1),e.Oqu(s)}}function H(i,a){1&i&&e._UZ(0,"span",9)}function ae(i,a){1&i&&e._UZ(0,"span",10)}function oe(i,a){if(1&i&&(e.TgZ(0,"button",6),e.ynx(1,2),e.YNc(2,H,1,0,"span",7),e.YNc(3,ae,1,0,"span",8),e.BQk(),e.qZA()),2&i){const s=e.oxw(2);e.Q6J("disabled",s.disabled),e.xp6(1),e.Q6J("ngSwitch",s.direction),e.xp6(1),e.Q6J("ngSwitchCase","rtl")}}function le(i,a){1&i&&e._UZ(0,"span",10)}function re(i,a){1&i&&e._UZ(0,"span",9)}function ce(i,a){if(1&i&&(e.TgZ(0,"button",6),e.ynx(1,2),e.YNc(2,le,1,0,"span",11),e.YNc(3,re,1,0,"span",12),e.BQk(),e.qZA()),2&i){const s=e.oxw(2);e.Q6J("disabled",s.disabled),e.xp6(1),e.Q6J("ngSwitch",s.direction),e.xp6(1),e.Q6J("ngSwitchCase","rtl")}}function fe(i,a){1&i&&e._UZ(0,"span",20)}function ze(i,a){1&i&&e._UZ(0,"span",21)}function me(i,a){if(1&i&&(e.ynx(0,2),e.YNc(1,fe,1,0,"span",18),e.YNc(2,ze,1,0,"span",19),e.BQk()),2&i){const s=e.oxw(4);e.Q6J("ngSwitch",s.direction),e.xp6(1),e.Q6J("ngSwitchCase","rtl")}}function Ce(i,a){1&i&&e._UZ(0,"span",21)}function xe(i,a){1&i&&e._UZ(0,"span",20)}function O(i,a){if(1&i&&(e.ynx(0,2),e.YNc(1,Ce,1,0,"span",22),e.YNc(2,xe,1,0,"span",23),e.BQk()),2&i){const s=e.oxw(4);e.Q6J("ngSwitch",s.direction),e.xp6(1),e.Q6J("ngSwitchCase","rtl")}}function R(i,a){if(1&i&&(e.TgZ(0,"div",15),e.ynx(1,2),e.YNc(2,me,3,2,"ng-container",16),e.YNc(3,O,3,2,"ng-container",16),e.BQk(),e.TgZ(4,"span",17),e._uU(5,"\u2022\u2022\u2022"),e.qZA()()),2&i){const s=e.oxw(2).$implicit;e.xp6(1),e.Q6J("ngSwitch",s),e.xp6(1),e.Q6J("ngSwitchCase","prev_5"),e.xp6(1),e.Q6J("ngSwitchCase","next_5")}}function b(i,a){if(1&i&&(e.ynx(0),e.TgZ(1,"a",13),e.YNc(2,R,6,3,"div",14),e.qZA(),e.BQk()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.Q6J("ngSwitch",s)}}function E(i,a){1&i&&(e.ynx(0,2),e.YNc(1,v,2,1,"a",3),e.YNc(2,oe,4,3,"button",4),e.YNc(3,ce,4,3,"button",4),e.YNc(4,b,3,1,"ng-container",5),e.BQk()),2&i&&(e.Q6J("ngSwitch",a.$implicit),e.xp6(1),e.Q6J("ngSwitchCase","page"),e.xp6(1),e.Q6J("ngSwitchCase","prev"),e.xp6(1),e.Q6J("ngSwitchCase","next"))}function F(i,a){}const d=function(i,a){return{$implicit:i,page:a}},x=["containerTemplate"];function C(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"ul")(1,"li",1),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.prePage())}),e.qZA(),e.TgZ(2,"li",2)(3,"input",3),e.NdJ("keydown.enter",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.jumpToPageViaInput(n))}),e.qZA(),e.TgZ(4,"span",4),e._uU(5,"/"),e.qZA(),e._uU(6),e.qZA(),e.TgZ(7,"li",5),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.nextPage())}),e.qZA()()}if(2&i){const s=e.oxw();e.xp6(1),e.Q6J("disabled",s.isFirstIndex)("direction",s.dir)("itemRender",s.itemRender),e.uIk("title",s.locale.prev_page),e.xp6(1),e.uIk("title",s.pageIndex+"/"+s.lastIndex),e.xp6(1),e.Q6J("disabled",s.disabled)("value",s.pageIndex),e.xp6(3),e.hij(" ",s.lastIndex," "),e.xp6(1),e.Q6J("disabled",s.isLastIndex)("direction",s.dir)("itemRender",s.itemRender),e.uIk("title",null==s.locale?null:s.locale.next_page)}}const D=["nz-pagination-options",""];function I(i,a){if(1&i&&e._UZ(0,"nz-option",4),2&i){const s=a.$implicit;e.Q6J("nzLabel",s.label)("nzValue",s.value)}}function te(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"nz-select",2),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.onPageSizeChange(n))}),e.YNc(1,I,1,2,"nz-option",3),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("nzDisabled",s.disabled)("nzSize",s.nzSize)("ngModel",s.pageSize),e.xp6(1),e.Q6J("ngForOf",s.listOfPageSizeOption)("ngForTrackBy",s.trackByOption)}}function he(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",5),e._uU(1),e.TgZ(2,"input",6),e.NdJ("keydown.enter",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.jumpToPageViaInput(n))}),e.qZA(),e._uU(3),e.qZA()}if(2&i){const s=e.oxw();e.xp6(1),e.hij(" ",s.locale.jump_to," "),e.xp6(1),e.Q6J("disabled",s.disabled),e.xp6(1),e.hij(" ",s.locale.page," ")}}function Le(i,a){}const Ze=function(i,a){return{$implicit:i,range:a}};function We(i,a){if(1&i&&(e.TgZ(0,"li",4),e.YNc(1,Le,0,0,"ng-template",5),e.qZA()),2&i){const s=e.oxw(2);e.xp6(1),e.Q6J("ngTemplateOutlet",s.showTotal)("ngTemplateOutletContext",e.WLB(2,Ze,s.total,s.ranges))}}function Ve(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",6),e.NdJ("gotoIndex",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.jumpPage(n))})("diffIndex",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.jumpDiff(n))}),e.qZA()}if(2&i){const s=a.$implicit,t=e.oxw(2);e.Q6J("locale",t.locale)("type",s.type)("index",s.index)("disabled",!!s.disabled)("itemRender",t.itemRender)("active",t.pageIndex===s.index)("direction",t.dir)}}function Ue(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",7),e.NdJ("pageIndexChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.onPageIndexChange(n))})("pageSizeChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.onPageSizeChange(n))}),e.qZA()}if(2&i){const s=e.oxw(2);e.Q6J("total",s.total)("locale",s.locale)("disabled",s.disabled)("nzSize",s.nzSize)("showSizeChanger",s.showSizeChanger)("showQuickJumper",s.showQuickJumper)("pageIndex",s.pageIndex)("pageSize",s.pageSize)("pageSizeOptions",s.pageSizeOptions)}}function He(i,a){if(1&i&&(e.TgZ(0,"ul"),e.YNc(1,We,2,5,"li",1),e.YNc(2,Ve,1,7,"li",2),e.YNc(3,Ue,1,9,"li",3),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngIf",s.showTotal),e.xp6(1),e.Q6J("ngForOf",s.listOfPageItem)("ngForTrackBy",s.trackByPageItem),e.xp6(1),e.Q6J("ngIf",s.showQuickJumper||s.showSizeChanger)}}function je(i,a){}function Ke(i,a){if(1&i&&(e.ynx(0),e.YNc(1,je,0,0,"ng-template",6),e.BQk()),2&i){e.oxw(2);const s=e.MAs(2);e.xp6(1),e.Q6J("ngTemplateOutlet",s.template)}}function Ge(i,a){if(1&i&&(e.ynx(0),e.YNc(1,Ke,2,1,"ng-container",5),e.BQk()),2&i){const s=e.oxw(),t=e.MAs(4);e.xp6(1),e.Q6J("ngIf",s.nzSimple)("ngIfElse",t.template)}}let De=(()=>{var i;class a{constructor(){this.active=!1,this.index=null,this.disabled=!1,this.direction="ltr",this.type=null,this.itemRender=null,this.diffIndex=new e.vpe,this.gotoIndex=new e.vpe,this.title=null}clickItem(){this.disabled||("page"===this.type?this.gotoIndex.emit(this.index):this.diffIndex.emit({next:1,prev:-1,prev_5:-5,next_5:5}[this.type]))}ngOnChanges(t){const{locale:n,index:o,type:c}=t;(n||o||c)&&(this.title={page:`${this.index}`,next:this.locale?.next_page,prev:this.locale?.prev_page,prev_5:this.locale?.prev_5,next_5:this.locale?.next_5}[this.type])}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["li","nz-pagination-item",""]],hostVars:19,hostBindings:function(t,n){1&t&&e.NdJ("click",function(){return n.clickItem()}),2&t&&(e.uIk("title",n.title),e.ekj("ant-pagination-prev","prev"===n.type)("ant-pagination-next","next"===n.type)("ant-pagination-item","page"===n.type)("ant-pagination-jump-prev","prev_5"===n.type)("ant-pagination-jump-prev-custom-icon","prev_5"===n.type)("ant-pagination-jump-next","next_5"===n.type)("ant-pagination-jump-next-custom-icon","next_5"===n.type)("ant-pagination-disabled",n.disabled)("ant-pagination-item-active",n.active))},inputs:{active:"active",locale:"locale",index:"index",disabled:"disabled",direction:"direction",type:"type",itemRender:"itemRender"},outputs:{diffIndex:"diffIndex",gotoIndex:"gotoIndex"},features:[e.TTD],attrs:f,decls:3,vars:5,consts:[["renderItemTemplate",""],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[3,"ngSwitch"],[4,"ngSwitchCase"],["type","button","class","ant-pagination-item-link",3,"disabled",4,"ngSwitchCase"],[4,"ngSwitchDefault"],["type","button",1,"ant-pagination-item-link",3,"disabled"],["nz-icon","","nzType","right",4,"ngSwitchCase"],["nz-icon","","nzType","left",4,"ngSwitchDefault"],["nz-icon","","nzType","right"],["nz-icon","","nzType","left"],["nz-icon","","nzType","left",4,"ngSwitchCase"],["nz-icon","","nzType","right",4,"ngSwitchDefault"],[1,"ant-pagination-item-link",3,"ngSwitch"],["class","ant-pagination-item-container",4,"ngSwitchDefault"],[1,"ant-pagination-item-container"],[3,"ngSwitch",4,"ngSwitchCase"],[1,"ant-pagination-item-ellipsis"],["nz-icon","","nzType","double-right","class","ant-pagination-item-link-icon",4,"ngSwitchCase"],["nz-icon","","nzType","double-left","class","ant-pagination-item-link-icon",4,"ngSwitchDefault"],["nz-icon","","nzType","double-right",1,"ant-pagination-item-link-icon"],["nz-icon","","nzType","double-left",1,"ant-pagination-item-link-icon"],["nz-icon","","nzType","double-left","class","ant-pagination-item-link-icon",4,"ngSwitchCase"],["nz-icon","","nzType","double-right","class","ant-pagination-item-link-icon",4,"ngSwitchDefault"]],template:function(t,n){if(1&t&&(e.YNc(0,E,5,4,"ng-template",null,0,e.W1O),e.YNc(2,F,0,0,"ng-template",1)),2&t){const o=e.MAs(1);e.xp6(2),e.Q6J("ngTemplateOutlet",n.itemRender||o)("ngTemplateOutletContext",e.WLB(2,d,n.type,n.index))}},dependencies:[S.tP,S.RF,S.n9,S.ED,U.Ls],encapsulation:2,changeDetection:0}),a})(),Xe=(()=>{var i;class a{constructor(t,n,o,c){this.cdr=t,this.renderer=n,this.elementRef=o,this.directionality=c,this.itemRender=null,this.disabled=!1,this.total=0,this.pageIndex=1,this.pageSize=10,this.pageIndexChange=new e.vpe,this.lastIndex=0,this.isFirstIndex=!1,this.isLastIndex=!1,this.dir="ltr",this.destroy$=new k.x,n.removeChild(n.parentNode(o.nativeElement),o.nativeElement)}ngOnInit(){this.directionality.change?.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.dir=t,this.updateRtlStyle(),this.cdr.detectChanges()}),this.dir=this.directionality.value,this.updateRtlStyle()}updateRtlStyle(){"rtl"===this.dir?this.renderer.addClass(this.elementRef.nativeElement,"ant-pagination-rtl"):this.renderer.removeClass(this.elementRef.nativeElement,"ant-pagination-rtl")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}jumpToPageViaInput(t){const n=t.target,o=(0,m.He)(n.value,this.pageIndex);this.onPageIndexChange(o),n.value=`${this.pageIndex}`}prePage(){this.onPageIndexChange(this.pageIndex-1)}nextPage(){this.onPageIndexChange(this.pageIndex+1)}onPageIndexChange(t){this.pageIndexChange.next(t)}updateBindingValue(){this.lastIndex=Math.ceil(this.total/this.pageSize),this.isFirstIndex=1===this.pageIndex,this.isLastIndex=this.pageIndex===this.lastIndex}ngOnChanges(t){const{pageIndex:n,total:o,pageSize:c}=t;(n||o||c)&&this.updateBindingValue()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(e.Qsj),e.Y36(e.SBq),e.Y36(_.Is,8))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-pagination-simple"]],viewQuery:function(t,n){if(1&t&&e.Gf(x,7),2&t){let o;e.iGM(o=e.CRH())&&(n.template=o.first)}},inputs:{itemRender:"itemRender",disabled:"disabled",locale:"locale",total:"total",pageIndex:"pageIndex",pageSize:"pageSize"},outputs:{pageIndexChange:"pageIndexChange"},features:[e.TTD],decls:2,vars:0,consts:[["containerTemplate",""],["nz-pagination-item","","type","prev",3,"disabled","direction","itemRender","click"],[1,"ant-pagination-simple-pager"],["size","3",3,"disabled","value","keydown.enter"],[1,"ant-pagination-slash"],["nz-pagination-item","","type","next",3,"disabled","direction","itemRender","click"]],template:function(t,n){1&t&&e.YNc(0,C,8,12,"ng-template",null,0,e.W1O)},dependencies:[De],encapsulation:2,changeDetection:0}),a})(),qe=(()=>{var i;class a{constructor(){this.nzSize="default",this.disabled=!1,this.showSizeChanger=!1,this.showQuickJumper=!1,this.total=0,this.pageIndex=1,this.pageSize=10,this.pageSizeOptions=[],this.pageIndexChange=new e.vpe,this.pageSizeChange=new e.vpe,this.listOfPageSizeOption=[]}onPageSizeChange(t){this.pageSize!==t&&this.pageSizeChange.next(t)}jumpToPageViaInput(t){const n=t.target,o=Math.floor((0,m.He)(n.value,this.pageIndex));this.pageIndexChange.next(o),n.value=""}trackByOption(t,n){return n.value}ngOnChanges(t){const{pageSize:n,pageSizeOptions:o,locale:c}=t;(n||o||c)&&(this.listOfPageSizeOption=[...new Set([...this.pageSizeOptions,this.pageSize])].map(u=>({value:u,label:`${u} ${this.locale.items_per_page}`})))}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["li","nz-pagination-options",""]],hostAttrs:[1,"ant-pagination-options"],inputs:{nzSize:"nzSize",disabled:"disabled",showSizeChanger:"showSizeChanger",showQuickJumper:"showQuickJumper",locale:"locale",total:"total",pageIndex:"pageIndex",pageSize:"pageSize",pageSizeOptions:"pageSizeOptions"},outputs:{pageIndexChange:"pageIndexChange",pageSizeChange:"pageSizeChange"},features:[e.TTD],attrs:D,decls:2,vars:2,consts:[["class","ant-pagination-options-size-changer",3,"nzDisabled","nzSize","ngModel","ngModelChange",4,"ngIf"],["class","ant-pagination-options-quick-jumper",4,"ngIf"],[1,"ant-pagination-options-size-changer",3,"nzDisabled","nzSize","ngModel","ngModelChange"],[3,"nzLabel","nzValue",4,"ngFor","ngForOf","ngForTrackBy"],[3,"nzLabel","nzValue"],[1,"ant-pagination-options-quick-jumper"],[3,"disabled","keydown.enter"]],template:function(t,n){1&t&&(e.YNc(0,te,2,5,"nz-select",0),e.YNc(1,he,4,3,"div",1)),2&t&&(e.Q6J("ngIf",n.showSizeChanger),e.xp6(1),e.Q6J("ngIf",n.showQuickJumper))},dependencies:[S.sg,S.O5,g.JJ,g.On,A.Ip,A.Vq],encapsulation:2,changeDetection:0}),a})(),et=(()=>{var i;class a{constructor(t,n,o,c){this.cdr=t,this.renderer=n,this.elementRef=o,this.directionality=c,this.nzSize="default",this.itemRender=null,this.showTotal=null,this.disabled=!1,this.showSizeChanger=!1,this.showQuickJumper=!1,this.total=0,this.pageIndex=1,this.pageSize=10,this.pageSizeOptions=[10,20,30,40],this.pageIndexChange=new e.vpe,this.pageSizeChange=new e.vpe,this.ranges=[0,0],this.listOfPageItem=[],this.dir="ltr",this.destroy$=new k.x,n.removeChild(n.parentNode(o.nativeElement),o.nativeElement)}ngOnInit(){this.directionality.change?.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.dir=t,this.updateRtlStyle(),this.cdr.detectChanges()}),this.dir=this.directionality.value,this.updateRtlStyle()}updateRtlStyle(){"rtl"===this.dir?this.renderer.addClass(this.elementRef.nativeElement,"ant-pagination-rtl"):this.renderer.removeClass(this.elementRef.nativeElement,"ant-pagination-rtl")}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}jumpPage(t){this.onPageIndexChange(t)}jumpDiff(t){this.jumpPage(this.pageIndex+t)}trackByPageItem(t,n){return`${n.type}-${n.index}`}onPageIndexChange(t){this.pageIndexChange.next(t)}onPageSizeChange(t){this.pageSizeChange.next(t)}getLastIndex(t,n){return Math.ceil(t/n)}buildIndexes(){const t=this.getLastIndex(this.total,this.pageSize);this.listOfPageItem=this.getListOfPageItem(this.pageIndex,t)}getListOfPageItem(t,n){const c=(u,y)=>{const w=[];for(let N=u;N<=y;N++)w.push({index:N,type:"page"});return w};return u=n<=9?c(1,n):((y,w)=>{let N=[];const B={type:"prev_5"},T={type:"next_5"},G=c(1,1),se=c(n,n);return N=y<5?[...c(2,4===y?6:5),T]:y{var i;class a{validatePageIndex(t,n){return t>n?n:t<1?1:t}onPageIndexChange(t){const n=this.getLastIndex(this.nzTotal,this.nzPageSize),o=this.validatePageIndex(t,n);o!==this.nzPageIndex&&!this.nzDisabled&&(this.nzPageIndex=o,this.nzPageIndexChange.emit(this.nzPageIndex))}onPageSizeChange(t){this.nzPageSize=t,this.nzPageSizeChange.emit(t);const n=this.getLastIndex(this.nzTotal,this.nzPageSize);this.nzPageIndex>n&&this.onPageIndexChange(n)}onTotalChange(t){const n=this.getLastIndex(t,this.nzPageSize);this.nzPageIndex>n&&Promise.resolve().then(()=>{this.onPageIndexChange(n),this.cdr.markForCheck()})}getLastIndex(t,n){return Math.ceil(t/n)}constructor(t,n,o,c,u){this.i18n=t,this.cdr=n,this.breakpointService=o,this.nzConfigService=c,this.directionality=u,this._nzModuleName="pagination",this.nzPageSizeChange=new e.vpe,this.nzPageIndexChange=new e.vpe,this.nzShowTotal=null,this.nzItemRender=null,this.nzSize="default",this.nzPageSizeOptions=[10,20,30,40],this.nzShowSizeChanger=!1,this.nzShowQuickJumper=!1,this.nzSimple=!1,this.nzDisabled=!1,this.nzResponsive=!1,this.nzHideOnSinglePage=!1,this.nzTotal=0,this.nzPageIndex=1,this.nzPageSize=10,this.showPagination=!0,this.size="default",this.dir="ltr",this.destroy$=new k.x,this.total$=new $.t(1)}ngOnInit(){this.i18n.localeChange.pipe((0,p.R)(this.destroy$)).subscribe(()=>{this.locale=this.i18n.getLocaleData("Pagination"),this.cdr.markForCheck()}),this.total$.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.onTotalChange(t)}),this.breakpointService.subscribe(z.WV).pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.nzResponsive&&(this.size=t===z.G_.xs?"small":"default",this.cdr.markForCheck())}),this.directionality.change?.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.dir=t,this.cdr.detectChanges()}),this.dir=this.directionality.value}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}ngOnChanges(t){const{nzHideOnSinglePage:n,nzTotal:o,nzPageSize:c,nzSize:u}=t;o&&this.total$.next(this.nzTotal),(n||o||c)&&(this.showPagination=this.nzHideOnSinglePage&&this.nzTotal>this.nzPageSize||this.nzTotal>0&&!this.nzHideOnSinglePage),u&&(this.size=u.currentValue)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(V.wi),e.Y36(e.sBO),e.Y36(z.r3),e.Y36(M.jY),e.Y36(_.Is,8))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-pagination"]],hostAttrs:[1,"ant-pagination"],hostVars:8,hostBindings:function(t,n){2&t&&e.ekj("ant-pagination-simple",n.nzSimple)("ant-pagination-disabled",n.nzDisabled)("mini",!n.nzSimple&&"small"===n.size)("ant-pagination-rtl","rtl"===n.dir)},inputs:{nzShowTotal:"nzShowTotal",nzItemRender:"nzItemRender",nzSize:"nzSize",nzPageSizeOptions:"nzPageSizeOptions",nzShowSizeChanger:"nzShowSizeChanger",nzShowQuickJumper:"nzShowQuickJumper",nzSimple:"nzSimple",nzDisabled:"nzDisabled",nzResponsive:"nzResponsive",nzHideOnSinglePage:"nzHideOnSinglePage",nzTotal:"nzTotal",nzPageIndex:"nzPageIndex",nzPageSize:"nzPageSize"},outputs:{nzPageSizeChange:"nzPageSizeChange",nzPageIndexChange:"nzPageIndexChange"},exportAs:["nzPagination"],features:[e.TTD],decls:5,vars:18,consts:[[4,"ngIf"],[3,"disabled","itemRender","locale","pageSize","total","pageIndex","pageIndexChange"],["simplePagination",""],[3,"nzSize","itemRender","showTotal","disabled","locale","showSizeChanger","showQuickJumper","total","pageIndex","pageSize","pageSizeOptions","pageIndexChange","pageSizeChange"],["defaultPagination",""],[4,"ngIf","ngIfElse"],[3,"ngTemplateOutlet"]],template:function(t,n){1&t&&(e.YNc(0,Ge,2,2,"ng-container",0),e.TgZ(1,"nz-pagination-simple",1,2),e.NdJ("pageIndexChange",function(c){return n.onPageIndexChange(c)}),e.qZA(),e.TgZ(3,"nz-pagination-default",3,4),e.NdJ("pageIndexChange",function(c){return n.onPageIndexChange(c)})("pageSizeChange",function(c){return n.onPageSizeChange(c)}),e.qZA()),2&t&&(e.Q6J("ngIf",n.showPagination),e.xp6(1),e.Q6J("disabled",n.nzDisabled)("itemRender",n.nzItemRender)("locale",n.locale)("pageSize",n.nzPageSize)("total",n.nzTotal)("pageIndex",n.nzPageIndex),e.xp6(2),e.Q6J("nzSize",n.size)("itemRender",n.nzItemRender)("showTotal",n.nzShowTotal)("disabled",n.nzDisabled)("locale",n.locale)("showSizeChanger",n.nzShowSizeChanger)("showQuickJumper",n.nzShowQuickJumper)("total",n.nzTotal)("pageIndex",n.nzPageIndex)("pageSize",n.nzPageSize)("pageSizeOptions",n.nzPageSizeOptions))},dependencies:[S.O5,S.tP,Xe,et],encapsulation:2,changeDetection:0}),(0,P.gn)([(0,M.oS)()],a.prototype,"nzSize",void 0),(0,P.gn)([(0,M.oS)()],a.prototype,"nzPageSizeOptions",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzShowSizeChanger",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzShowQuickJumper",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzSimple",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzDisabled",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzResponsive",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzHideOnSinglePage",void 0),(0,P.gn)([(0,m.Rn)()],a.prototype,"nzTotal",void 0),(0,P.gn)([(0,m.Rn)()],a.prototype,"nzPageIndex",void 0),(0,P.gn)([(0,m.Rn)()],a.prototype,"nzPageSize",void 0),a})(),it=(()=>{var i;class a{}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275mod=e.oAB({type:i}),i.\u0275inj=e.cJS({imports:[_.vT,S.ez,g.u5,A.LV,V.YI,U.PV]}),a})();var Pe=h(57907),Me=h(82669),Se=h(92438),j=h(65619),ne=h(52572),pe=h(63019),Te=h(36232),st=h(22096),K=h(37398),ye=h(93997),Ie=h(83620),at=h(836),ve=h(32181),X=h(94664),ie=h(27921),be=h(60932),Oe=h(21631),Fe=h(70855),ot=h(41958);const ge=["*"];function lt(i,a){}function rt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"label",15),e.NdJ("ngModelChange",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw(2);return e.KtG(o.check(n))}),e.qZA()}if(2&i){const s=e.oxw().$implicit;e.Q6J("ngModel",s.checked)}}function ct(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"label",16),e.NdJ("ngModelChange",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw(2);return e.KtG(o.check(n))}),e.qZA()}if(2&i){const s=e.oxw().$implicit;e.Q6J("ngModel",s.checked)}}function dt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",12),e.NdJ("click",function(){const o=e.CHM(s).$implicit,c=e.oxw(2);return e.KtG(c.check(o))}),e.YNc(1,rt,1,1,"label",13),e.YNc(2,ct,1,1,"label",14),e.TgZ(3,"span"),e._uU(4),e.qZA()()}if(2&i){const s=a.$implicit,t=e.oxw(2);e.Q6J("nzSelected",s.checked),e.xp6(1),e.Q6J("ngIf",!t.filterMultiple),e.xp6(1),e.Q6J("ngIf",t.filterMultiple),e.xp6(2),e.Oqu(s.text)}}function ht(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"nz-filter-trigger",3),e.NdJ("nzVisibleChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.onVisibleChange(n))}),e._UZ(2,"span",4),e.qZA(),e.TgZ(3,"nz-dropdown-menu",null,5)(5,"div",6)(6,"ul",7),e.YNc(7,dt,5,4,"li",8),e.qZA(),e.TgZ(8,"div",9)(9,"button",10),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.reset())}),e._uU(10),e.qZA(),e.TgZ(11,"button",11),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.confirm())}),e._uU(12),e.qZA()()()(),e.BQk()}if(2&i){const s=e.MAs(4),t=e.oxw();e.xp6(1),e.Q6J("nzVisible",t.isVisible)("nzActive",t.isChecked)("nzDropdownMenu",s),e.xp6(6),e.Q6J("ngForOf",t.listOfParsedFilter)("ngForTrackBy",t.trackByValue),e.xp6(2),e.Q6J("disabled",!t.isChecked),e.xp6(1),e.hij(" ",t.locale.filterReset," "),e.xp6(2),e.Oqu(t.locale.filterConfirm)}}function gt(i,a){}function ut(i,a){if(1&i&&e._UZ(0,"span",6),2&i){const s=e.oxw();e.ekj("active","ascend"===s.sortOrder)}}function _t(i,a){if(1&i&&e._UZ(0,"span",7),2&i){const s=e.oxw();e.ekj("active","descend"===s.sortOrder)}}const ft=["nzColumnKey",""];function zt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"nz-table-filter",5),e.NdJ("filterChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.onFilterValueChange(n))}),e.qZA()}if(2&i){const s=e.oxw(),t=e.MAs(2),n=e.MAs(4);e.Q6J("contentTemplate",t)("extraTemplate",n)("customFilter",s.nzCustomFilter)("filterMultiple",s.nzFilterMultiple)("listOfFilter",s.nzFilters)}}function mt(i,a){}function Ct(i,a){if(1&i&&e.YNc(0,mt,0,0,"ng-template",6),2&i){const s=e.oxw(),t=e.MAs(6),n=e.MAs(8);e.Q6J("ngTemplateOutlet",s.nzShowSort?t:n)}}function xt(i,a){1&i&&(e.Hsn(0),e.Hsn(1,1))}function vt(i,a){if(1&i&&e._UZ(0,"nz-table-sorters",7),2&i){const s=e.oxw(),t=e.MAs(8);e.Q6J("sortOrder",s.sortOrder)("sortDirections",s.sortDirections)("contentTemplate",t)}}function St(i,a){1&i&&e.Hsn(0,2)}const Tt=[[["","nz-th-extra",""]],[["nz-filter-trigger"]],"*"],yt=["[nz-th-extra]","nz-filter-trigger","*"],bt=["nz-table-content",""];function Ot(i,a){if(1&i&&e._UZ(0,"col"),2&i){const s=a.$implicit;e.Udp("width",s)("min-width",s)}}function wt(i,a){}function Dt(i,a){if(1&i&&(e.TgZ(0,"thead",3),e.YNc(1,wt,0,0,"ng-template",2),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngTemplateOutlet",s.theadTemplate)}}function Pt(i,a){}const ke=["tdElement"],Mt=["nz-table-fixed-row",""];function It(i,a){}function Ft(i,a){if(1&i&&(e.TgZ(0,"div",4),e.ALo(1,"async"),e.YNc(2,It,0,0,"ng-template",5),e.qZA()),2&i){const s=e.oxw(),t=e.MAs(5);e.Udp("width",e.lcZ(1,3,s.hostWidth$),"px"),e.xp6(2),e.Q6J("ngTemplateOutlet",t)}}function kt(i,a){1&i&&e.Hsn(0)}const Nt=["nz-table-measure-row",""];function Et(i,a){1&i&&e._UZ(0,"td",1,2)}function Rt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"tr",3),e.NdJ("listOfAutoWidth",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.onListOfAutoWidthChange(n))}),e.qZA()}if(2&i){const s=e.oxw().ngIf;e.Q6J("listOfMeasureColumn",s)}}function Bt(i,a){if(1&i&&(e.ynx(0),e.YNc(1,Rt,1,1,"tr",2),e.BQk()),2&i){const s=a.ngIf,t=e.oxw();e.xp6(1),e.Q6J("ngIf",t.isInsideTable&&s.length)}}function At(i,a){if(1&i&&(e.TgZ(0,"tr",4),e._UZ(1,"nz-embed-empty",5),e.ALo(2,"async"),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("specificContent",e.lcZ(2,1,s.noResult$))}}const $t=["tableHeaderElement"],Qt=["tableBodyElement"];function Yt(i,a){if(1&i&&(e.TgZ(0,"div",7,8),e._UZ(2,"table",9),e.qZA()),2&i){const s=e.oxw(2);e.Q6J("ngStyle",s.bodyStyleMap),e.xp6(2),e.Q6J("scrollX",s.scrollX)("listOfColWidth",s.listOfColWidth)("contentTemplate",s.contentTemplate)}}function Jt(i,a){}const Lt=function(i,a){return{$implicit:i,index:a}};function Zt(i,a){if(1&i&&(e.ynx(0),e.YNc(1,Jt,0,0,"ng-template",13),e.BQk()),2&i){const s=a.$implicit,t=a.index,n=e.oxw(3);e.xp6(1),e.Q6J("ngTemplateOutlet",n.virtualTemplate)("ngTemplateOutletContext",e.WLB(2,Lt,s,t))}}function Wt(i,a){if(1&i&&(e.TgZ(0,"cdk-virtual-scroll-viewport",10,8)(2,"table",11)(3,"tbody"),e.YNc(4,Zt,2,5,"ng-container",12),e.qZA()()()),2&i){const s=e.oxw(2);e.Udp("height",s.data.length?s.scrollY:s.noDateVirtualHeight),e.Q6J("itemSize",s.virtualItemSize)("maxBufferPx",s.virtualMaxBufferPx)("minBufferPx",s.virtualMinBufferPx),e.xp6(2),e.Q6J("scrollX",s.scrollX)("listOfColWidth",s.listOfColWidth),e.xp6(2),e.Q6J("cdkVirtualForOf",s.data)("cdkVirtualForTrackBy",s.virtualForTrackBy)}}function Vt(i,a){if(1&i&&(e.ynx(0),e.TgZ(1,"div",2,3),e._UZ(3,"table",4),e.qZA(),e.YNc(4,Yt,3,4,"div",5),e.YNc(5,Wt,5,9,"cdk-virtual-scroll-viewport",6),e.BQk()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngStyle",s.headerStyleMap),e.xp6(2),e.Q6J("scrollX",s.scrollX)("listOfColWidth",s.listOfColWidth)("theadTemplate",s.theadTemplate),e.xp6(1),e.Q6J("ngIf",!s.virtualTemplate),e.xp6(1),e.Q6J("ngIf",s.virtualTemplate)}}function Ut(i,a){if(1&i&&(e.TgZ(0,"div",14,8),e._UZ(2,"table",15),e.qZA()),2&i){const s=e.oxw();e.Q6J("ngStyle",s.bodyStyleMap),e.xp6(2),e.Q6J("scrollX",s.scrollX)("listOfColWidth",s.listOfColWidth)("theadTemplate",s.theadTemplate)("contentTemplate",s.contentTemplate)}}function Ht(i,a){if(1&i&&(e.ynx(0),e._uU(1),e.BQk()),2&i){const s=e.oxw();e.xp6(1),e.Oqu(s.title)}}function jt(i,a){if(1&i&&(e.ynx(0),e._uU(1),e.BQk()),2&i){const s=e.oxw();e.xp6(1),e.Oqu(s.footer)}}function Kt(i,a){}function Gt(i,a){if(1&i&&(e.ynx(0),e.YNc(1,Kt,0,0,"ng-template",10),e.BQk()),2&i){e.oxw();const s=e.MAs(11);e.xp6(1),e.Q6J("ngTemplateOutlet",s)}}function Xt(i,a){if(1&i&&e._UZ(0,"nz-table-title-footer",11),2&i){const s=e.oxw();e.Q6J("title",s.nzTitle)}}function qt(i,a){if(1&i&&e._UZ(0,"nz-table-inner-scroll",12),2&i){const s=e.oxw(),t=e.MAs(13),n=e.MAs(3);e.Q6J("data",s.data)("scrollX",s.scrollX)("scrollY",s.scrollY)("contentTemplate",t)("listOfColWidth",s.listOfAutoColWidth)("theadTemplate",s.theadTemplate)("verticalScrollBarWidth",s.verticalScrollBarWidth)("virtualTemplate",s.nzVirtualScrollDirective?s.nzVirtualScrollDirective.templateRef:null)("virtualItemSize",s.nzVirtualItemSize)("virtualMaxBufferPx",s.nzVirtualMaxBufferPx)("virtualMinBufferPx",s.nzVirtualMinBufferPx)("tableMainElement",n)("virtualForTrackBy",s.nzVirtualForTrackBy)}}function en(i,a){if(1&i&&e._UZ(0,"nz-table-inner-default",13),2&i){const s=e.oxw(),t=e.MAs(13);e.Q6J("tableLayout",s.nzTableLayout)("listOfColWidth",s.listOfManualColWidth)("theadTemplate",s.theadTemplate)("contentTemplate",t)}}function tn(i,a){if(1&i&&e._UZ(0,"nz-table-title-footer",14),2&i){const s=e.oxw();e.Q6J("footer",s.nzFooter)}}function nn(i,a){}function sn(i,a){if(1&i&&(e.ynx(0),e.YNc(1,nn,0,0,"ng-template",10),e.BQk()),2&i){e.oxw();const s=e.MAs(11);e.xp6(1),e.Q6J("ngTemplateOutlet",s)}}function an(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"nz-pagination",16),e.NdJ("nzPageSizeChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.onPageSizeChange(n))})("nzPageIndexChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.onPageIndexChange(n))}),e.qZA()}if(2&i){const s=e.oxw(2);e.Q6J("hidden",!s.showPagination)("nzShowSizeChanger",s.nzShowSizeChanger)("nzPageSizeOptions",s.nzPageSizeOptions)("nzItemRender",s.nzItemRender)("nzShowQuickJumper",s.nzShowQuickJumper)("nzHideOnSinglePage",s.nzHideOnSinglePage)("nzShowTotal",s.nzShowTotal)("nzSize","small"===s.nzPaginationType?"small":"default"===s.nzSize?"default":"small")("nzPageSize",s.nzPageSize)("nzTotal",s.nzTotal)("nzSimple",s.nzSimple)("nzPageIndex",s.nzPageIndex)}}function on(i,a){if(1&i&&e.YNc(0,an,1,12,"nz-pagination",15),2&i){const s=e.oxw();e.Q6J("ngIf",s.nzShowPagination&&s.data.length)}}function ln(i,a){1&i&&e.Hsn(0)}const rn=["contentTemplate"];function cn(i,a){1&i&&e.Hsn(0)}function dn(i,a){}function hn(i,a){if(1&i&&(e.ynx(0),e.YNc(1,dn,0,0,"ng-template",2),e.BQk()),2&i){e.oxw();const s=e.MAs(1);e.xp6(1),e.Q6J("ngTemplateOutlet",s)}}let Ne=(()=>{var i;class a{onVisibleChange(t){this.nzVisible=t,this.nzVisibleChange.next(t)}hide(){this.nzVisible=!1,this.cdr.markForCheck()}show(){this.nzVisible=!0,this.cdr.markForCheck()}constructor(t,n,o,c){this.nzConfigService=t,this.ngZone=n,this.cdr=o,this.destroy$=c,this._nzModuleName="filterTrigger",this.nzActive=!1,this.nzVisible=!1,this.nzBackdrop=!1,this.nzVisibleChange=new e.vpe}ngOnInit(){this.ngZone.runOutsideAngular(()=>{(0,Se.R)(this.nzDropdown.nativeElement,"click").pipe((0,p.R)(this.destroy$)).subscribe(t=>{t.stopPropagation()})})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(M.jY),e.Y36(e.R0b),e.Y36(e.sBO),e.Y36(z.kn))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-filter-trigger"]],viewQuery:function(t,n){if(1&t&&e.Gf(Z.cm,7,e.SBq),2&t){let o;e.iGM(o=e.CRH())&&(n.nzDropdown=o.first)}},inputs:{nzActive:"nzActive",nzDropdownMenu:"nzDropdownMenu",nzVisible:"nzVisible",nzBackdrop:"nzBackdrop"},outputs:{nzVisibleChange:"nzVisibleChange"},exportAs:["nzFilterTrigger"],features:[e._Bn([z.kn])],ngContentSelectors:ge,decls:2,vars:8,consts:[["nz-dropdown","","nzTrigger","click","nzPlacement","bottomRight",1,"ant-table-filter-trigger",3,"nzBackdrop","nzClickHide","nzDropdownMenu","nzVisible","nzVisibleChange"]],template:function(t,n){1&t&&(e.F$t(),e.TgZ(0,"span",0),e.NdJ("nzVisibleChange",function(c){return n.onVisibleChange(c)}),e.Hsn(1),e.qZA()),2&t&&(e.ekj("active",n.nzActive)("ant-table-filter-open",n.nzVisible),e.Q6J("nzBackdrop",n.nzBackdrop)("nzClickHide",!1)("nzDropdownMenu",n.nzDropdownMenu)("nzVisible",n.nzVisible))},dependencies:[Z.cm],encapsulation:2,changeDetection:0}),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzBackdrop",void 0),a})(),gn=(()=>{var i;class a{trackByValue(t,n){return n.value}check(t){this.filterMultiple?(this.listOfParsedFilter=this.listOfParsedFilter.map(n=>n===t?{...n,checked:!t.checked}:n),t.checked=!t.checked):this.listOfParsedFilter=this.listOfParsedFilter.map(n=>({...n,checked:n===t})),this.isChecked=this.getCheckedStatus(this.listOfParsedFilter)}confirm(){this.isVisible=!1,this.emitFilterData()}reset(){this.isVisible=!1,this.listOfParsedFilter=this.parseListOfFilter(this.listOfFilter,!0),this.isChecked=this.getCheckedStatus(this.listOfParsedFilter),this.emitFilterData()}onVisibleChange(t){this.isVisible=t,t?this.listOfChecked=this.listOfParsedFilter.filter(n=>n.checked).map(n=>n.value):this.emitFilterData()}emitFilterData(){const t=this.listOfParsedFilter.filter(n=>n.checked).map(n=>n.value);(0,m.cO)(this.listOfChecked,t)||this.filterChange.emit(this.filterMultiple?t:t.length>0?t[0]:null)}parseListOfFilter(t,n){return t.map(o=>({text:o.text,value:o.value,checked:!n&&!!o.byDefault}))}getCheckedStatus(t){return t.some(n=>n.checked)}constructor(t,n){this.cdr=t,this.i18n=n,this.contentTemplate=null,this.customFilter=!1,this.extraTemplate=null,this.filterMultiple=!0,this.listOfFilter=[],this.filterChange=new e.vpe,this.destroy$=new k.x,this.isChecked=!1,this.isVisible=!1,this.listOfParsedFilter=[],this.listOfChecked=[]}ngOnInit(){this.i18n.localeChange.pipe((0,p.R)(this.destroy$)).subscribe(()=>{this.locale=this.i18n.getLocaleData("Table"),this.cdr.markForCheck()})}ngOnChanges(t){const{listOfFilter:n}=t;n&&this.listOfFilter&&this.listOfFilter.length&&(this.listOfParsedFilter=this.parseListOfFilter(this.listOfFilter),this.isChecked=this.getCheckedStatus(this.listOfParsedFilter))}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(V.wi))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table-filter"]],hostAttrs:[1,"ant-table-filter-column"],inputs:{contentTemplate:"contentTemplate",customFilter:"customFilter",extraTemplate:"extraTemplate",filterMultiple:"filterMultiple",listOfFilter:"listOfFilter"},outputs:{filterChange:"filterChange"},features:[e.TTD],decls:3,vars:3,consts:[[1,"ant-table-column-title"],[3,"ngTemplateOutlet"],[4,"ngIf","ngIfElse"],[3,"nzVisible","nzActive","nzDropdownMenu","nzVisibleChange"],["nz-icon","","nzType","filter","nzTheme","fill"],["filterMenu","nzDropdownMenu"],[1,"ant-table-filter-dropdown"],["nz-menu",""],["nz-menu-item","",3,"nzSelected","click",4,"ngFor","ngForOf","ngForTrackBy"],[1,"ant-table-filter-dropdown-btns"],["nz-button","","nzType","link","nzSize","small",3,"disabled","click"],["nz-button","","nzType","primary","nzSize","small",3,"click"],["nz-menu-item","",3,"nzSelected","click"],["nz-radio","",3,"ngModel","ngModelChange",4,"ngIf"],["nz-checkbox","",3,"ngModel","ngModelChange",4,"ngIf"],["nz-radio","",3,"ngModel","ngModelChange"],["nz-checkbox","",3,"ngModel","ngModelChange"]],template:function(t,n){1&t&&(e.TgZ(0,"span",0),e.YNc(1,lt,0,0,"ng-template",1),e.qZA(),e.YNc(2,ht,13,8,"ng-container",2)),2&t&&(e.xp6(1),e.Q6J("ngTemplateOutlet",n.contentTemplate),e.xp6(1),e.Q6J("ngIf",!n.customFilter)("ngIfElse",n.extraTemplate))},dependencies:[ee.wO,ee.r9,g.JJ,g.On,Pe.Of,J.Ie,Z.RR,Y.ix,Fe.w,ot.dQ,S.sg,S.O5,S.tP,U.Ls,Ne],encapsulation:2,changeDetection:0}),a})(),un=(()=>{var i;class a{constructor(){this.sortDirections=["ascend","descend",null],this.sortOrder=null,this.contentTemplate=null,this.isUp=!1,this.isDown=!1}ngOnChanges(t){const{sortDirections:n}=t;n&&(this.isUp=-1!==this.sortDirections.indexOf("ascend"),this.isDown=-1!==this.sortDirections.indexOf("descend"))}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table-sorters"]],hostAttrs:[1,"ant-table-column-sorters"],inputs:{sortDirections:"sortDirections",sortOrder:"sortOrder",contentTemplate:"contentTemplate"},features:[e.TTD],decls:6,vars:5,consts:[[1,"ant-table-column-title"],[3,"ngTemplateOutlet"],[1,"ant-table-column-sorter"],[1,"ant-table-column-sorter-inner"],["nz-icon","","nzType","caret-up","class","ant-table-column-sorter-up",3,"active",4,"ngIf"],["nz-icon","","nzType","caret-down","class","ant-table-column-sorter-down",3,"active",4,"ngIf"],["nz-icon","","nzType","caret-up",1,"ant-table-column-sorter-up"],["nz-icon","","nzType","caret-down",1,"ant-table-column-sorter-down"]],template:function(t,n){1&t&&(e.TgZ(0,"span",0),e.YNc(1,gt,0,0,"ng-template",1),e.qZA(),e.TgZ(2,"span",2)(3,"span",3),e.YNc(4,ut,1,2,"span",4),e.YNc(5,_t,1,2,"span",5),e.qZA()()),2&t&&(e.xp6(1),e.Q6J("ngTemplateOutlet",n.contentTemplate),e.xp6(1),e.ekj("ant-table-column-sorter-full",n.isDown&&n.isUp),e.xp6(2),e.Q6J("ngIf",n.isUp),e.xp6(1),e.Q6J("ngIf",n.isDown))},dependencies:[Fe.w,S.O5,S.tP,U.Ls],encapsulation:2,changeDetection:0}),a})(),Ee=(()=>{var i;class a{setAutoLeftWidth(t){this.renderer.setStyle(this.elementRef.nativeElement,"left",t)}setAutoRightWidth(t){this.renderer.setStyle(this.elementRef.nativeElement,"right",t)}setIsFirstRight(t){this.setFixClass(t,"ant-table-cell-fix-right-first")}setIsLastLeft(t){this.setFixClass(t,"ant-table-cell-fix-left-last")}setFixClass(t,n){this.renderer.removeClass(this.elementRef.nativeElement,n),t&&this.renderer.addClass(this.elementRef.nativeElement,n)}constructor(t,n){this.renderer=t,this.elementRef=n,this.nzRight=!1,this.nzLeft=!1,this.colspan=null,this.colSpan=null,this.changes$=new k.x,this.isAutoLeft=!1,this.isAutoRight=!1,this.isFixedLeft=!1,this.isFixedRight=!1,this.isFixed=!1}ngOnChanges(){this.setIsFirstRight(!1),this.setIsLastLeft(!1),this.isAutoLeft=""===this.nzLeft||!0===this.nzLeft,this.isAutoRight=""===this.nzRight||!0===this.nzRight,this.isFixedLeft=!1!==this.nzLeft,this.isFixedRight=!1!==this.nzRight,this.isFixed=this.isFixedLeft||this.isFixedRight;const t=n=>"string"==typeof n&&""!==n?n:null;this.setAutoLeftWidth(t(this.nzLeft)),this.setAutoRightWidth(t(this.nzRight)),this.changes$.next()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.Qsj),e.Y36(e.SBq))},i.\u0275dir=e.lG2({type:i,selectors:[["td","nzRight",""],["th","nzRight",""],["td","nzLeft",""],["th","nzLeft",""]],hostVars:6,hostBindings:function(t,n){2&t&&(e.Udp("position",n.isFixed?"sticky":null),e.ekj("ant-table-cell-fix-right",n.isFixedRight)("ant-table-cell-fix-left",n.isFixedLeft))},inputs:{nzRight:"nzRight",nzLeft:"nzLeft",colspan:"colspan",colSpan:"colSpan"},features:[e.TTD]}),a})(),de=(()=>{var i;class a{setTheadTemplate(t){this.theadTemplate$.next(t)}setHasFixLeft(t){this.hasFixLeft$.next(t)}setHasFixRight(t){this.hasFixRight$.next(t)}setTableWidthConfig(t){this.tableWidthConfigPx$.next(t)}setListOfTh(t){let n=0;t.forEach(c=>{n+=c.colspan&&+c.colspan||c.colSpan&&+c.colSpan||1});const o=t.map(c=>c.nzWidth);this.columnCount$.next(n),this.listOfThWidthConfigPx$.next(o)}setListOfMeasureColumn(t){const n=[];t.forEach(o=>{const c=o.colspan&&+o.colspan||o.colSpan&&+o.colSpan||1;for(let u=0;u`${n}px`))}setShowEmpty(t){this.showEmpty$.next(t)}setNoResult(t){this.noResult$.next(t)}setScroll(t,n){const o=!(!t&&!n);o||this.setListOfAutoWidth([]),this.enableAutoMeasure$.next(o)}constructor(){this.theadTemplate$=new $.t(1),this.hasFixLeft$=new $.t(1),this.hasFixRight$=new $.t(1),this.hostWidth$=new $.t(1),this.columnCount$=new $.t(1),this.showEmpty$=new $.t(1),this.noResult$=new $.t(1),this.listOfThWidthConfigPx$=new j.X([]),this.tableWidthConfigPx$=new j.X([]),this.manualWidthConfigPx$=(0,ne.a)([this.tableWidthConfigPx$,this.listOfThWidthConfigPx$]).pipe((0,K.U)(([t,n])=>t.length?t:n)),this.listOfAutoWidthPx$=new $.t(1),this.listOfListOfThWidthPx$=(0,pe.T)(this.manualWidthConfigPx$,(0,ne.a)([this.listOfAutoWidthPx$,this.manualWidthConfigPx$]).pipe((0,K.U)(([t,n])=>t.length===n.length?t.map((o,c)=>"0px"===o?n[c]||null:n[c]||o):n))),this.listOfMeasureColumn$=new $.t(1),this.listOfListOfThWidth$=this.listOfAutoWidthPx$.pipe((0,K.U)(t=>t.map(n=>parseInt(n,10)))),this.enableAutoMeasure$=new $.t(1)}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac}),a})(),_n=(()=>{var i;class a{constructor(t){this.isInsideTable=!1,this.isInsideTable=!!t}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(de,8))},i.\u0275dir=e.lG2({type:i,selectors:[["th",9,"nz-disable-th",3,"mat-cell",""],["td",9,"nz-disable-td",3,"mat-cell",""]],hostVars:2,hostBindings:function(t,n){2&t&&e.ekj("ant-table-cell",n.isInsideTable)}}),a})(),we=(()=>{var i;class a{updatePageSize(t){this.pageSize$.next(t)}updateFrontPagination(t){this.frontPagination$.next(t)}updatePageIndex(t){this.pageIndex$.next(t)}updateListOfData(t){this.listOfData$.next(t)}updateListOfCustomColumn(t){this.listOfCustomColumn$.next(t)}constructor(){this.destroy$=new k.x,this.pageIndex$=new j.X(1),this.frontPagination$=new j.X(!0),this.pageSize$=new j.X(10),this.listOfData$=new j.X([]),this.listOfCustomColumn$=new j.X([]),this.pageIndexDistinct$=this.pageIndex$.pipe((0,ye.x)()),this.pageSizeDistinct$=this.pageSize$.pipe((0,ye.x)()),this.listOfCalcOperator$=new j.X([]),this.queryParams$=(0,ne.a)([this.pageIndexDistinct$,this.pageSizeDistinct$,this.listOfCalcOperator$]).pipe((0,Ie.b)(0),(0,at.T)(1),(0,K.U)(([t,n,o])=>({pageIndex:t,pageSize:n,sort:o.filter(c=>c.sortFn).map(c=>({key:c.key,value:c.sortOrder})),filter:o.filter(c=>c.filterFn).map(c=>({key:c.key,value:c.filterValue}))}))),this.listOfDataAfterCalc$=(0,ne.a)([this.listOfData$,this.listOfCalcOperator$]).pipe((0,K.U)(([t,n])=>{let o=[...t];const c=n.filter(y=>{const{filterValue:w,filterFn:N}=y;return!(null==w||Array.isArray(w)&&0===w.length)&&"function"==typeof N});for(const y of c){const{filterFn:w,filterValue:N}=y;o=o.filter(B=>w(N,B))}const u=n.filter(y=>null!==y.sortOrder&&"function"==typeof y.sortFn).sort((y,w)=>+w.sortPriority-+y.sortPriority);return n.length&&o.sort((y,w)=>{for(const N of u){const{sortFn:B,sortOrder:T}=N;if(B&&T){const G=B(y,w,T);if(0!==G)return"ascend"===T?G:-G}}return 0}),o})),this.listOfFrontEndCurrentPageData$=(0,ne.a)([this.pageIndexDistinct$,this.pageSizeDistinct$,this.listOfDataAfterCalc$]).pipe((0,p.R)(this.destroy$),(0,ve.h)(t=>{const[n,o,c]=t;return n<=(Math.ceil(c.length/o)||1)}),(0,K.U)(([t,n,o])=>o.slice((t-1)*n,t*n))),this.listOfCurrentPageData$=this.frontPagination$.pipe((0,X.w)(t=>t?this.listOfFrontEndCurrentPageData$:this.listOfDataAfterCalc$)),this.total$=this.frontPagination$.pipe((0,X.w)(t=>t?this.listOfDataAfterCalc$:this.listOfData$),(0,K.U)(t=>t.length),(0,ye.x)())}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac}),a})(),Re=(()=>{var i;class a{getNextSortDirection(t,n){const o=t.indexOf(n);return o===t.length-1?t[0]:t[o+1]}setSortOrder(t){this.sortOrderChange$.next(t)}clearSortOrder(){null!==this.sortOrder&&this.setSortOrder(null)}onFilterValueChange(t){this.nzFilterChange.emit(t),this.nzFilterValue=t,this.updateCalcOperator()}updateCalcOperator(){this.calcOperatorChange$.next()}constructor(t,n,o,c){this.host=t,this.cdr=n,this.ngZone=o,this.destroy$=c,this.manualClickOrder$=new k.x,this.calcOperatorChange$=new k.x,this.nzFilterValue=null,this.sortOrder=null,this.sortDirections=["ascend","descend",null],this.sortOrderChange$=new k.x,this.isNzShowSortChanged=!1,this.isNzShowFilterChanged=!1,this.nzFilterMultiple=!0,this.nzSortOrder=null,this.nzSortPriority=!1,this.nzSortDirections=["ascend","descend",null],this.nzFilters=[],this.nzSortFn=null,this.nzFilterFn=null,this.nzShowSort=!1,this.nzShowFilter=!1,this.nzCustomFilter=!1,this.nzCheckedChange=new e.vpe,this.nzSortOrderChange=new e.vpe,this.nzFilterChange=new e.vpe}ngOnInit(){this.ngZone.runOutsideAngular(()=>(0,Se.R)(this.host.nativeElement,"click").pipe((0,ve.h)(()=>this.nzShowSort),(0,p.R)(this.destroy$)).subscribe(()=>{const t=this.getNextSortDirection(this.sortDirections,this.sortOrder);this.ngZone.run(()=>{this.setSortOrder(t),this.manualClickOrder$.next(this)})})),this.sortOrderChange$.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.sortOrder!==t&&(this.sortOrder=t,this.nzSortOrderChange.emit(t)),this.updateCalcOperator(),this.cdr.markForCheck()})}ngOnChanges(t){const{nzSortDirections:n,nzFilters:o,nzSortOrder:c,nzSortFn:u,nzFilterFn:y,nzSortPriority:w,nzFilterMultiple:N,nzShowSort:B,nzShowFilter:T}=t;n&&this.nzSortDirections&&this.nzSortDirections.length&&(this.sortDirections=this.nzSortDirections),c&&(this.sortOrder=this.nzSortOrder,this.setSortOrder(this.nzSortOrder)),B&&(this.isNzShowSortChanged=!0),T&&(this.isNzShowFilterChanged=!0);const G=se=>se&&se.firstChange&&void 0!==se.currentValue;if((G(c)||G(u))&&!this.isNzShowSortChanged&&(this.nzShowSort=!0),G(o)&&!this.isNzShowFilterChanged&&(this.nzShowFilter=!0),(o||N)&&this.nzShowFilter){const se=this.nzFilters.filter(ue=>ue.byDefault).map(ue=>ue.value);this.nzFilterValue=this.nzFilterMultiple?se:se[0]||null}(u||y||w||o)&&this.updateCalcOperator()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.SBq),e.Y36(e.sBO),e.Y36(e.R0b),e.Y36(z.kn))},i.\u0275cmp=e.Xpm({type:i,selectors:[["th","nzColumnKey",""],["th","nzSortFn",""],["th","nzSortOrder",""],["th","nzFilters",""],["th","nzShowSort",""],["th","nzShowFilter",""],["th","nzCustomFilter",""]],hostVars:4,hostBindings:function(t,n){2&t&&e.ekj("ant-table-column-has-sorters",n.nzShowSort)("ant-table-column-sort","descend"===n.sortOrder||"ascend"===n.sortOrder)},inputs:{nzColumnKey:"nzColumnKey",nzFilterMultiple:"nzFilterMultiple",nzSortOrder:"nzSortOrder",nzSortPriority:"nzSortPriority",nzSortDirections:"nzSortDirections",nzFilters:"nzFilters",nzSortFn:"nzSortFn",nzFilterFn:"nzFilterFn",nzShowSort:"nzShowSort",nzShowFilter:"nzShowFilter",nzCustomFilter:"nzCustomFilter"},outputs:{nzCheckedChange:"nzCheckedChange",nzSortOrderChange:"nzSortOrderChange",nzFilterChange:"nzFilterChange"},features:[e._Bn([z.kn]),e.TTD],attrs:ft,ngContentSelectors:yt,decls:9,vars:2,consts:[[3,"contentTemplate","extraTemplate","customFilter","filterMultiple","listOfFilter","filterChange",4,"ngIf","ngIfElse"],["notFilterTemplate",""],["extraTemplate",""],["sortTemplate",""],["contentTemplate",""],[3,"contentTemplate","extraTemplate","customFilter","filterMultiple","listOfFilter","filterChange"],[3,"ngTemplateOutlet"],[3,"sortOrder","sortDirections","contentTemplate"]],template:function(t,n){if(1&t&&(e.F$t(Tt),e.YNc(0,zt,1,5,"nz-table-filter",0),e.YNc(1,Ct,1,1,"ng-template",null,1,e.W1O),e.YNc(3,xt,2,0,"ng-template",null,2,e.W1O),e.YNc(5,vt,1,3,"ng-template",null,3,e.W1O),e.YNc(7,St,1,0,"ng-template",null,4,e.W1O)),2&t){const o=e.MAs(2);e.Q6J("ngIf",n.nzShowFilter||n.nzCustomFilter)("ngIfElse",o)}},dependencies:[S.O5,S.tP,un,gn],encapsulation:2,changeDetection:0}),(0,P.gn)([(0,m.yF)()],a.prototype,"nzShowSort",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzShowFilter",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzCustomFilter",void 0),a})(),Be=(()=>{var i;class a{constructor(t,n){this.renderer=t,this.elementRef=n,this.changes$=new k.x,this.nzWidth=null,this.colspan=null,this.colSpan=null,this.rowspan=null,this.rowSpan=null}ngOnChanges(t){const{nzWidth:n,colspan:o,rowspan:c,colSpan:u,rowSpan:y}=t;if(o||u){const w=this.colspan||this.colSpan;(0,m.kK)(w)?this.renderer.removeAttribute(this.elementRef.nativeElement,"colspan"):this.renderer.setAttribute(this.elementRef.nativeElement,"colspan",`${w}`)}if(c||y){const w=this.rowspan||this.rowSpan;(0,m.kK)(w)?this.renderer.removeAttribute(this.elementRef.nativeElement,"rowspan"):this.renderer.setAttribute(this.elementRef.nativeElement,"rowspan",`${w}`)}(n||o)&&this.changes$.next()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.Qsj),e.Y36(e.SBq))},i.\u0275dir=e.lG2({type:i,selectors:[["th"]],inputs:{nzWidth:"nzWidth",colspan:"colspan",colSpan:"colSpan",rowspan:"rowspan",rowSpan:"rowSpan"},features:[e.TTD]}),a})(),fn=(()=>{var i;class a{constructor(){this.nzAlign=null}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275dir=e.lG2({type:i,selectors:[["th","nzAlign",""],["td","nzAlign",""]],hostVars:2,hostBindings:function(t,n){2&t&&e.Udp("text-align",n.nzAlign)},inputs:{nzAlign:"nzAlign"}}),a})(),Ae=(()=>{var i;class a{constructor(){this.tableLayout="auto",this.theadTemplate=null,this.contentTemplate=null,this.listOfColWidth=[],this.scrollX=null}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["table","nz-table-content",""]],hostVars:8,hostBindings:function(t,n){2&t&&(e.Udp("table-layout",n.tableLayout)("width",n.scrollX)("min-width",n.scrollX?"100%":null),e.ekj("ant-table-fixed",n.scrollX))},inputs:{tableLayout:"tableLayout",theadTemplate:"theadTemplate",contentTemplate:"contentTemplate",listOfColWidth:"listOfColWidth",scrollX:"scrollX"},attrs:bt,ngContentSelectors:ge,decls:4,vars:3,consts:[[3,"width","minWidth",4,"ngFor","ngForOf"],["class","ant-table-thead",4,"ngIf"],[3,"ngTemplateOutlet"],[1,"ant-table-thead"]],template:function(t,n){1&t&&(e.F$t(),e.YNc(0,Ot,1,4,"col",0),e.YNc(1,Dt,2,1,"thead",1),e.YNc(2,Pt,0,0,"ng-template",2),e.Hsn(3)),2&t&&(e.Q6J("ngForOf",n.listOfColWidth),e.xp6(1),e.Q6J("ngIf",n.theadTemplate),e.xp6(1),e.Q6J("ngTemplateOutlet",n.contentTemplate))},dependencies:[S.sg,S.O5,S.tP],encapsulation:2,changeDetection:0}),a})(),zn=(()=>{var i;class a{constructor(t,n){this.nzTableStyleService=t,this.renderer=n,this.hostWidth$=new j.X(null),this.enableAutoMeasure$=new j.X(!1),this.destroy$=new k.x}ngOnInit(){if(this.nzTableStyleService){const{enableAutoMeasure$:t,hostWidth$:n}=this.nzTableStyleService;t.pipe((0,p.R)(this.destroy$)).subscribe(this.enableAutoMeasure$),n.pipe((0,p.R)(this.destroy$)).subscribe(this.hostWidth$)}}ngAfterViewInit(){this.nzTableStyleService.columnCount$.pipe((0,p.R)(this.destroy$)).subscribe(t=>{this.renderer.setAttribute(this.tdElement.nativeElement,"colspan",`${t}`)})}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(de),e.Y36(e.Qsj))},i.\u0275cmp=e.Xpm({type:i,selectors:[["tr","nz-table-fixed-row",""],["tr","nzExpand",""]],viewQuery:function(t,n){if(1&t&&e.Gf(ke,7),2&t){let o;e.iGM(o=e.CRH())&&(n.tdElement=o.first)}},attrs:Mt,ngContentSelectors:ge,decls:6,vars:4,consts:[[1,"nz-disable-td","ant-table-cell"],["tdElement",""],["class","ant-table-expanded-row-fixed","style","position: sticky; left: 0px; overflow: hidden;",3,"width",4,"ngIf","ngIfElse"],["contentTemplate",""],[1,"ant-table-expanded-row-fixed",2,"position","sticky","left","0px","overflow","hidden"],[3,"ngTemplateOutlet"]],template:function(t,n){if(1&t&&(e.F$t(),e.TgZ(0,"td",0,1),e.YNc(2,Ft,3,5,"div",2),e.ALo(3,"async"),e.qZA(),e.YNc(4,kt,1,0,"ng-template",null,3,e.W1O)),2&t){const o=e.MAs(5);e.xp6(2),e.Q6J("ngIf",e.lcZ(3,2,n.enableAutoMeasure$))("ngIfElse",o)}},dependencies:[S.O5,S.tP,S.Ov],encapsulation:2,changeDetection:0}),a})(),mn=(()=>{var i;class a{constructor(){this.tableLayout="auto",this.listOfColWidth=[],this.theadTemplate=null,this.contentTemplate=null}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table-inner-default"]],hostAttrs:[1,"ant-table-container"],inputs:{tableLayout:"tableLayout",listOfColWidth:"listOfColWidth",theadTemplate:"theadTemplate",contentTemplate:"contentTemplate"},decls:2,vars:4,consts:[[1,"ant-table-content"],["nz-table-content","",3,"contentTemplate","tableLayout","listOfColWidth","theadTemplate"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0),e._UZ(1,"table",1),e.qZA()),2&t&&(e.xp6(1),e.Q6J("contentTemplate",n.contentTemplate)("tableLayout",n.tableLayout)("listOfColWidth",n.listOfColWidth)("theadTemplate",n.theadTemplate))},dependencies:[Ae],encapsulation:2,changeDetection:0}),a})(),Cn=(()=>{var i;class a{constructor(t,n){this.nzResizeObserver=t,this.ngZone=n,this.listOfMeasureColumn=[],this.listOfAutoWidth=new e.vpe,this.destroy$=new k.x}trackByFunc(t,n){return n}ngAfterViewInit(){this.listOfTdElement.changes.pipe((0,ie.O)(this.listOfTdElement)).pipe((0,X.w)(t=>(0,ne.a)(t.toArray().map(n=>this.nzResizeObserver.observe(n).pipe((0,K.U)(([o])=>{const{width:c}=o.target.getBoundingClientRect();return Math.floor(c)}))))),(0,Ie.b)(16),(0,p.R)(this.destroy$)).subscribe(t=>{this.ngZone instanceof e.R0b&&e.R0b.isInAngularZone()?this.listOfAutoWidth.next(t):this.ngZone.run(()=>this.listOfAutoWidth.next(t))})}ngOnDestroy(){this.destroy$.next(!0),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(Q.D3),e.Y36(e.R0b))},i.\u0275cmp=e.Xpm({type:i,selectors:[["tr","nz-table-measure-row",""]],viewQuery:function(t,n){if(1&t&&e.Gf(ke,5),2&t){let o;e.iGM(o=e.CRH())&&(n.listOfTdElement=o)}},hostAttrs:[1,"ant-table-measure-now"],inputs:{listOfMeasureColumn:"listOfMeasureColumn"},outputs:{listOfAutoWidth:"listOfAutoWidth"},attrs:Nt,decls:1,vars:2,consts:[["class","nz-disable-td","style","padding: 0px; border: 0px; height: 0px;",4,"ngFor","ngForOf","ngForTrackBy"],[1,"nz-disable-td",2,"padding","0px","border","0px","height","0px"],["tdElement",""]],template:function(t,n){1&t&&e.YNc(0,Et,2,0,"td",0),2&t&&e.Q6J("ngForOf",n.listOfMeasureColumn)("ngForTrackBy",n.trackByFunc)},dependencies:[S.sg],encapsulation:2,changeDetection:0}),a})(),$e=(()=>{var i;class a{constructor(t){if(this.nzTableStyleService=t,this.isInsideTable=!1,this.showEmpty$=new j.X(!1),this.noResult$=new j.X(void 0),this.listOfMeasureColumn$=new j.X([]),this.destroy$=new k.x,this.isInsideTable=!!this.nzTableStyleService,this.nzTableStyleService){const{showEmpty$:n,noResult$:o,listOfMeasureColumn$:c}=this.nzTableStyleService;o.pipe((0,p.R)(this.destroy$)).subscribe(this.noResult$),c.pipe((0,p.R)(this.destroy$)).subscribe(this.listOfMeasureColumn$),n.pipe((0,p.R)(this.destroy$)).subscribe(this.showEmpty$)}}onListOfAutoWidthChange(t){this.nzTableStyleService.setListOfAutoWidth(t)}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(de,8))},i.\u0275cmp=e.Xpm({type:i,selectors:[["tbody"]],hostVars:2,hostBindings:function(t,n){2&t&&e.ekj("ant-table-tbody",n.isInsideTable)},ngContentSelectors:ge,decls:5,vars:6,consts:[[4,"ngIf"],["class","ant-table-placeholder","nz-table-fixed-row","",4,"ngIf"],["nz-table-measure-row","",3,"listOfMeasureColumn","listOfAutoWidth",4,"ngIf"],["nz-table-measure-row","",3,"listOfMeasureColumn","listOfAutoWidth"],["nz-table-fixed-row","",1,"ant-table-placeholder"],["nzComponentName","table",3,"specificContent"]],template:function(t,n){1&t&&(e.F$t(),e.YNc(0,Bt,2,1,"ng-container",0),e.ALo(1,"async"),e.Hsn(2),e.YNc(3,At,3,3,"tr",1),e.ALo(4,"async")),2&t&&(e.Q6J("ngIf",e.lcZ(1,2,n.listOfMeasureColumn$)),e.xp6(3),e.Q6J("ngIf",e.lcZ(4,4,n.showEmpty$)))},dependencies:[S.O5,W.gB,Cn,zn,S.Ov],encapsulation:2,changeDetection:0}),a})(),Qe=(()=>{var i;class a{setScrollPositionClassName(t=!1){const{scrollWidth:n,scrollLeft:o,clientWidth:c}=this.tableBodyElement.nativeElement,u="ant-table-ping-left",y="ant-table-ping-right";n===c&&0!==n||t?(this.renderer.removeClass(this.tableMainElement,u),this.renderer.removeClass(this.tableMainElement,y)):0===o?(this.renderer.removeClass(this.tableMainElement,u),this.renderer.addClass(this.tableMainElement,y)):n===o+c?(this.renderer.removeClass(this.tableMainElement,y),this.renderer.addClass(this.tableMainElement,u)):(this.renderer.addClass(this.tableMainElement,u),this.renderer.addClass(this.tableMainElement,y))}constructor(t,n,o,c){this.renderer=t,this.ngZone=n,this.platform=o,this.resizeService=c,this.data=[],this.scrollX=null,this.scrollY=null,this.contentTemplate=null,this.widthConfig=[],this.listOfColWidth=[],this.theadTemplate=null,this.virtualTemplate=null,this.virtualItemSize=0,this.virtualMaxBufferPx=200,this.virtualMinBufferPx=100,this.virtualForTrackBy=u=>u,this.headerStyleMap={},this.bodyStyleMap={},this.verticalScrollBarWidth=0,this.noDateVirtualHeight="182px",this.data$=new k.x,this.scroll$=new k.x,this.destroy$=new k.x}ngOnChanges(t){const{scrollX:n,scrollY:o,data:c}=t;(n||o)&&(this.headerStyleMap={overflowX:"hidden",overflowY:this.scrollY&&0!==this.verticalScrollBarWidth?"scroll":"hidden"},this.bodyStyleMap={overflowY:this.scrollY?"scroll":"hidden",overflowX:this.scrollX?"auto":null,maxHeight:this.scrollY},this.ngZone.runOutsideAngular(()=>this.scroll$.next())),c&&this.ngZone.runOutsideAngular(()=>this.data$.next())}ngAfterViewInit(){this.platform.isBrowser&&this.ngZone.runOutsideAngular(()=>{const t=this.scroll$.pipe((0,ie.O)(null),(0,be.g)(0),(0,X.w)(()=>(0,Se.R)(this.tableBodyElement.nativeElement,"scroll").pipe((0,ie.O)(!0))),(0,p.R)(this.destroy$)),n=this.resizeService.subscribe().pipe((0,p.R)(this.destroy$)),o=this.data$.pipe((0,p.R)(this.destroy$));(0,pe.T)(t,n,o,this.scroll$).pipe((0,ie.O)(!0),(0,be.g)(0),(0,p.R)(this.destroy$)).subscribe(()=>this.setScrollPositionClassName()),t.pipe((0,ve.h)(()=>!!this.scrollY)).subscribe(()=>this.tableHeaderElement.nativeElement.scrollLeft=this.tableBodyElement.nativeElement.scrollLeft)})}ngOnDestroy(){this.setScrollPositionClassName(!0),this.destroy$.next(),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.Qsj),e.Y36(e.R0b),e.Y36(l.t4),e.Y36(z.rI))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table-inner-scroll"]],viewQuery:function(t,n){if(1&t&&(e.Gf($t,5,e.SBq),e.Gf(Qt,5,e.SBq),e.Gf(r.N7,5,r.N7)),2&t){let o;e.iGM(o=e.CRH())&&(n.tableHeaderElement=o.first),e.iGM(o=e.CRH())&&(n.tableBodyElement=o.first),e.iGM(o=e.CRH())&&(n.cdkVirtualScrollViewport=o.first)}},hostAttrs:[1,"ant-table-container"],inputs:{data:"data",scrollX:"scrollX",scrollY:"scrollY",contentTemplate:"contentTemplate",widthConfig:"widthConfig",listOfColWidth:"listOfColWidth",theadTemplate:"theadTemplate",virtualTemplate:"virtualTemplate",virtualItemSize:"virtualItemSize",virtualMaxBufferPx:"virtualMaxBufferPx",virtualMinBufferPx:"virtualMinBufferPx",tableMainElement:"tableMainElement",virtualForTrackBy:"virtualForTrackBy",verticalScrollBarWidth:"verticalScrollBarWidth"},features:[e.TTD],decls:2,vars:2,consts:[[4,"ngIf"],["class","ant-table-content",3,"ngStyle",4,"ngIf"],[1,"ant-table-header","nz-table-hide-scrollbar",3,"ngStyle"],["tableHeaderElement",""],["nz-table-content","","tableLayout","fixed",3,"scrollX","listOfColWidth","theadTemplate"],["class","ant-table-body",3,"ngStyle",4,"ngIf"],[3,"itemSize","maxBufferPx","minBufferPx","height",4,"ngIf"],[1,"ant-table-body",3,"ngStyle"],["tableBodyElement",""],["nz-table-content","","tableLayout","fixed",3,"scrollX","listOfColWidth","contentTemplate"],[3,"itemSize","maxBufferPx","minBufferPx"],["nz-table-content","","tableLayout","fixed",3,"scrollX","listOfColWidth"],[4,"cdkVirtualFor","cdkVirtualForOf","cdkVirtualForTrackBy"],[3,"ngTemplateOutlet","ngTemplateOutletContext"],[1,"ant-table-content",3,"ngStyle"],["nz-table-content","","tableLayout","fixed",3,"scrollX","listOfColWidth","theadTemplate","contentTemplate"]],template:function(t,n){1&t&&(e.YNc(0,Vt,6,6,"ng-container",0),e.YNc(1,Ut,3,5,"div",1)),2&t&&(e.Q6J("ngIf",n.scrollY),e.xp6(1),e.Q6J("ngIf",!n.scrollY))},dependencies:[S.O5,S.tP,S.PC,r.xd,r.x0,r.N7,$e,Ae],encapsulation:2,changeDetection:0}),a})(),xn=(()=>{var i;class a{constructor(t){this.templateRef=t}static ngTemplateContextGuard(t,n){return!0}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.Rgc))},i.\u0275dir=e.lG2({type:i,selectors:[["","nz-virtual-scroll",""]],exportAs:["nzVirtualScroll"]}),a})(),vn=(()=>{var i;class a{constructor(){this.title=null,this.footer=null}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table-title-footer"]],hostVars:4,hostBindings:function(t,n){2&t&&e.ekj("ant-table-title",null!==n.title)("ant-table-footer",null!==n.footer)},inputs:{title:"title",footer:"footer"},decls:2,vars:2,consts:[[4,"nzStringTemplateOutlet"]],template:function(t,n){1&t&&(e.YNc(0,Ht,2,1,"ng-container",0),e.YNc(1,jt,2,1,"ng-container",0)),2&t&&(e.Q6J("nzStringTemplateOutlet",n.title),e.xp6(1),e.Q6J("nzStringTemplateOutlet",n.footer))},dependencies:[L.f],encapsulation:2,changeDetection:0}),a})(),Sn=(()=>{var i;class a{onPageSizeChange(t){this.nzTableDataService.updatePageSize(t)}onPageIndexChange(t){this.nzTableDataService.updatePageIndex(t)}constructor(t,n,o,c,u,y,w){this.elementRef=t,this.nzResizeObserver=n,this.nzConfigService=o,this.cdr=c,this.nzTableStyleService=u,this.nzTableDataService=y,this.directionality=w,this._nzModuleName="table",this.nzTableLayout="auto",this.nzShowTotal=null,this.nzItemRender=null,this.nzTitle=null,this.nzFooter=null,this.nzNoResult=void 0,this.nzPageSizeOptions=[10,20,30,40,50],this.nzVirtualItemSize=0,this.nzVirtualMaxBufferPx=200,this.nzVirtualMinBufferPx=100,this.nzVirtualForTrackBy=N=>N,this.nzLoadingDelay=0,this.nzPageIndex=1,this.nzPageSize=10,this.nzTotal=0,this.nzWidthConfig=[],this.nzData=[],this.nzCustomColumn=[],this.nzPaginationPosition="bottom",this.nzScroll={x:null,y:null},this.nzPaginationType="default",this.nzFrontPagination=!0,this.nzTemplateMode=!1,this.nzShowPagination=!0,this.nzLoading=!1,this.nzOuterBordered=!1,this.nzLoadingIndicator=null,this.nzBordered=!1,this.nzSize="default",this.nzShowSizeChanger=!1,this.nzHideOnSinglePage=!1,this.nzShowQuickJumper=!1,this.nzSimple=!1,this.nzPageSizeChange=new e.vpe,this.nzPageIndexChange=new e.vpe,this.nzQueryParams=new e.vpe,this.nzCurrentPageDataChange=new e.vpe,this.nzCustomColumnChange=new e.vpe,this.data=[],this.scrollX=null,this.scrollY=null,this.theadTemplate=null,this.listOfAutoColWidth=[],this.listOfManualColWidth=[],this.hasFixLeft=!1,this.hasFixRight=!1,this.showPagination=!0,this.destroy$=new k.x,this.templateMode$=new j.X(!1),this.dir="ltr",this.verticalScrollBarWidth=0,this.nzConfigService.getConfigChangeEventForComponent("table").pipe((0,p.R)(this.destroy$)).subscribe(()=>{this.cdr.markForCheck()})}ngOnInit(){const{pageIndexDistinct$:t,pageSizeDistinct$:n,listOfCurrentPageData$:o,total$:c,queryParams$:u,listOfCustomColumn$:y}=this.nzTableDataService,{theadTemplate$:w,hasFixLeft$:N,hasFixRight$:B}=this.nzTableStyleService;this.dir=this.directionality.value,this.directionality.change?.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.dir=T,this.cdr.detectChanges()}),u.pipe((0,p.R)(this.destroy$)).subscribe(this.nzQueryParams),t.pipe((0,p.R)(this.destroy$)).subscribe(T=>{T!==this.nzPageIndex&&(this.nzPageIndex=T,this.nzPageIndexChange.next(T))}),n.pipe((0,p.R)(this.destroy$)).subscribe(T=>{T!==this.nzPageSize&&(this.nzPageSize=T,this.nzPageSizeChange.next(T))}),c.pipe((0,p.R)(this.destroy$),(0,ve.h)(()=>this.nzFrontPagination)).subscribe(T=>{T!==this.nzTotal&&(this.nzTotal=T,this.cdr.markForCheck())}),o.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.data=T,this.nzCurrentPageDataChange.next(T),this.cdr.markForCheck()}),y.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.nzCustomColumn=T,this.nzCustomColumnChange.next(T),this.cdr.markForCheck()}),w.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.theadTemplate=T,this.cdr.markForCheck()}),N.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.hasFixLeft=T,this.cdr.markForCheck()}),B.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.hasFixRight=T,this.cdr.markForCheck()}),(0,ne.a)([c,this.templateMode$]).pipe((0,K.U)(([T,G])=>0===T&&!G),(0,p.R)(this.destroy$)).subscribe(T=>{this.nzTableStyleService.setShowEmpty(T)}),this.verticalScrollBarWidth=(0,m.D8)("vertical"),this.nzTableStyleService.listOfListOfThWidthPx$.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.listOfAutoColWidth=T,this.cdr.markForCheck()}),this.nzTableStyleService.manualWidthConfigPx$.pipe((0,p.R)(this.destroy$)).subscribe(T=>{this.listOfManualColWidth=T,this.cdr.markForCheck()})}ngOnChanges(t){const{nzScroll:n,nzPageIndex:o,nzPageSize:c,nzFrontPagination:u,nzData:y,nzCustomColumn:w,nzWidthConfig:N,nzNoResult:B,nzTemplateMode:T}=t;o&&this.nzTableDataService.updatePageIndex(this.nzPageIndex),c&&this.nzTableDataService.updatePageSize(this.nzPageSize),y&&(this.nzData=this.nzData||[],this.nzTableDataService.updateListOfData(this.nzData)),w&&(this.nzCustomColumn=this.nzCustomColumn||[],this.nzTableDataService.updateListOfCustomColumn(this.nzCustomColumn)),u&&this.nzTableDataService.updateFrontPagination(this.nzFrontPagination),n&&this.setScrollOnChanges(),N&&this.nzTableStyleService.setTableWidthConfig(this.nzWidthConfig),T&&this.templateMode$.next(this.nzTemplateMode),B&&this.nzTableStyleService.setNoResult(this.nzNoResult),this.updateShowPagination()}ngAfterViewInit(){this.nzResizeObserver.observe(this.elementRef).pipe((0,K.U)(([t])=>{const{width:n}=t.target.getBoundingClientRect();return Math.floor(n-(this.scrollY?this.verticalScrollBarWidth:0))}),(0,p.R)(this.destroy$)).subscribe(this.nzTableStyleService.hostWidth$),this.nzTableInnerScrollComponent&&this.nzTableInnerScrollComponent.cdkVirtualScrollViewport&&(this.cdkVirtualScrollViewport=this.nzTableInnerScrollComponent.cdkVirtualScrollViewport)}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}setScrollOnChanges(){this.scrollX=this.nzScroll&&this.nzScroll.x||null,this.scrollY=this.nzScroll&&this.nzScroll.y||null,this.nzTableStyleService.setScroll(this.scrollX,this.scrollY)}updateShowPagination(){this.showPagination=this.nzHideOnSinglePage&&this.nzData.length>this.nzPageSize||this.nzData.length>0&&!this.nzHideOnSinglePage||!this.nzFrontPagination&&this.nzTotal>this.nzPageSize}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.SBq),e.Y36(Q.D3),e.Y36(M.jY),e.Y36(e.sBO),e.Y36(de),e.Y36(we),e.Y36(_.Is,8))},i.\u0275cmp=e.Xpm({type:i,selectors:[["nz-table"]],contentQueries:function(t,n,o){if(1&t&&e.Suo(o,xn,5),2&t){let c;e.iGM(c=e.CRH())&&(n.nzVirtualScrollDirective=c.first)}},viewQuery:function(t,n){if(1&t&&e.Gf(Qe,5),2&t){let o;e.iGM(o=e.CRH())&&(n.nzTableInnerScrollComponent=o.first)}},hostAttrs:[1,"ant-table-wrapper"],hostVars:4,hostBindings:function(t,n){2&t&&e.ekj("ant-table-wrapper-rtl","rtl"===n.dir)("ant-table-custom-column",n.nzCustomColumn.length)},inputs:{nzTableLayout:"nzTableLayout",nzShowTotal:"nzShowTotal",nzItemRender:"nzItemRender",nzTitle:"nzTitle",nzFooter:"nzFooter",nzNoResult:"nzNoResult",nzPageSizeOptions:"nzPageSizeOptions",nzVirtualItemSize:"nzVirtualItemSize",nzVirtualMaxBufferPx:"nzVirtualMaxBufferPx",nzVirtualMinBufferPx:"nzVirtualMinBufferPx",nzVirtualForTrackBy:"nzVirtualForTrackBy",nzLoadingDelay:"nzLoadingDelay",nzPageIndex:"nzPageIndex",nzPageSize:"nzPageSize",nzTotal:"nzTotal",nzWidthConfig:"nzWidthConfig",nzData:"nzData",nzCustomColumn:"nzCustomColumn",nzPaginationPosition:"nzPaginationPosition",nzScroll:"nzScroll",nzPaginationType:"nzPaginationType",nzFrontPagination:"nzFrontPagination",nzTemplateMode:"nzTemplateMode",nzShowPagination:"nzShowPagination",nzLoading:"nzLoading",nzOuterBordered:"nzOuterBordered",nzLoadingIndicator:"nzLoadingIndicator",nzBordered:"nzBordered",nzSize:"nzSize",nzShowSizeChanger:"nzShowSizeChanger",nzHideOnSinglePage:"nzHideOnSinglePage",nzShowQuickJumper:"nzShowQuickJumper",nzSimple:"nzSimple"},outputs:{nzPageSizeChange:"nzPageSizeChange",nzPageIndexChange:"nzPageIndexChange",nzQueryParams:"nzQueryParams",nzCurrentPageDataChange:"nzCurrentPageDataChange",nzCustomColumnChange:"nzCustomColumnChange"},exportAs:["nzTable"],features:[e._Bn([de,we]),e.TTD],ngContentSelectors:ge,decls:14,vars:27,consts:[[3,"nzDelay","nzSpinning","nzIndicator"],[4,"ngIf"],[1,"ant-table"],["tableMainElement",""],[3,"title",4,"ngIf"],[3,"data","scrollX","scrollY","contentTemplate","listOfColWidth","theadTemplate","verticalScrollBarWidth","virtualTemplate","virtualItemSize","virtualMaxBufferPx","virtualMinBufferPx","tableMainElement","virtualForTrackBy",4,"ngIf","ngIfElse"],["defaultTemplate",""],[3,"footer",4,"ngIf"],["paginationTemplate",""],["contentTemplate",""],[3,"ngTemplateOutlet"],[3,"title"],[3,"data","scrollX","scrollY","contentTemplate","listOfColWidth","theadTemplate","verticalScrollBarWidth","virtualTemplate","virtualItemSize","virtualMaxBufferPx","virtualMinBufferPx","tableMainElement","virtualForTrackBy"],[3,"tableLayout","listOfColWidth","theadTemplate","contentTemplate"],[3,"footer"],["class","ant-table-pagination ant-table-pagination-right",3,"hidden","nzShowSizeChanger","nzPageSizeOptions","nzItemRender","nzShowQuickJumper","nzHideOnSinglePage","nzShowTotal","nzSize","nzPageSize","nzTotal","nzSimple","nzPageIndex","nzPageSizeChange","nzPageIndexChange",4,"ngIf"],[1,"ant-table-pagination","ant-table-pagination-right",3,"hidden","nzShowSizeChanger","nzPageSizeOptions","nzItemRender","nzShowQuickJumper","nzHideOnSinglePage","nzShowTotal","nzSize","nzPageSize","nzTotal","nzSimple","nzPageIndex","nzPageSizeChange","nzPageIndexChange"]],template:function(t,n){if(1&t&&(e.F$t(),e.TgZ(0,"nz-spin",0),e.YNc(1,Gt,2,1,"ng-container",1),e.TgZ(2,"div",2,3),e.YNc(4,Xt,1,1,"nz-table-title-footer",4),e.YNc(5,qt,1,13,"nz-table-inner-scroll",5),e.YNc(6,en,1,4,"ng-template",null,6,e.W1O),e.YNc(8,tn,1,1,"nz-table-title-footer",7),e.qZA(),e.YNc(9,sn,2,1,"ng-container",1),e.qZA(),e.YNc(10,on,1,1,"ng-template",null,8,e.W1O),e.YNc(12,ln,1,0,"ng-template",null,9,e.W1O)),2&t){const o=e.MAs(7);e.Q6J("nzDelay",n.nzLoadingDelay)("nzSpinning",n.nzLoading)("nzIndicator",n.nzLoadingIndicator),e.xp6(1),e.Q6J("ngIf","both"===n.nzPaginationPosition||"top"===n.nzPaginationPosition),e.xp6(1),e.ekj("ant-table-rtl","rtl"===n.dir)("ant-table-fixed-header",n.nzData.length&&n.scrollY)("ant-table-fixed-column",n.scrollX)("ant-table-has-fix-left",n.hasFixLeft)("ant-table-has-fix-right",n.hasFixRight)("ant-table-bordered",n.nzBordered)("nz-table-out-bordered",n.nzOuterBordered&&!n.nzBordered)("ant-table-middle","middle"===n.nzSize)("ant-table-small","small"===n.nzSize),e.xp6(2),e.Q6J("ngIf",n.nzTitle),e.xp6(1),e.Q6J("ngIf",n.scrollY||n.scrollX)("ngIfElse",o),e.xp6(3),e.Q6J("ngIf",n.nzFooter),e.xp6(1),e.Q6J("ngIf","both"===n.nzPaginationPosition||"bottom"===n.nzPaginationPosition)}},dependencies:[S.O5,S.tP,nt,Me.W,vn,mn,Qe],encapsulation:2,changeDetection:0}),(0,P.gn)([(0,m.yF)()],a.prototype,"nzFrontPagination",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzTemplateMode",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzShowPagination",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzLoading",void 0),(0,P.gn)([(0,m.yF)()],a.prototype,"nzOuterBordered",void 0),(0,P.gn)([(0,M.oS)()],a.prototype,"nzLoadingIndicator",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzBordered",void 0),(0,P.gn)([(0,M.oS)()],a.prototype,"nzSize",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzShowSizeChanger",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzHideOnSinglePage",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzShowQuickJumper",void 0),(0,P.gn)([(0,M.oS)(),(0,m.yF)()],a.prototype,"nzSimple",void 0),a})(),Je=(()=>{var i;class a{constructor(t){this.nzTableStyleService=t,this.destroy$=new k.x,this.listOfFixedColumns$=new $.t(1),this.listOfColumns$=new $.t(1),this.listOfFixedColumnsChanges$=this.listOfFixedColumns$.pipe((0,X.w)(n=>(0,pe.T)(this.listOfFixedColumns$,...n.map(o=>o.changes$)).pipe((0,Oe.z)(()=>this.listOfFixedColumns$))),(0,p.R)(this.destroy$)),this.listOfFixedLeftColumnChanges$=this.listOfFixedColumnsChanges$.pipe((0,K.U)(n=>n.filter(o=>!1!==o.nzLeft))),this.listOfFixedRightColumnChanges$=this.listOfFixedColumnsChanges$.pipe((0,K.U)(n=>n.filter(o=>!1!==o.nzRight))),this.listOfColumnsChanges$=this.listOfColumns$.pipe((0,X.w)(n=>(0,pe.T)(this.listOfColumns$,...n.map(o=>o.changes$)).pipe((0,Oe.z)(()=>this.listOfColumns$))),(0,p.R)(this.destroy$)),this.isInsideTable=!1,this.isInsideTable=!!t}ngAfterContentInit(){this.nzTableStyleService&&(this.listOfCellFixedDirective.changes.pipe((0,ie.O)(this.listOfCellFixedDirective),(0,p.R)(this.destroy$)).subscribe(this.listOfFixedColumns$),this.listOfNzThDirective.changes.pipe((0,ie.O)(this.listOfNzThDirective),(0,p.R)(this.destroy$)).subscribe(this.listOfColumns$),this.listOfFixedLeftColumnChanges$.subscribe(t=>{t.forEach(n=>n.setIsLastLeft(n===t[t.length-1]))}),this.listOfFixedRightColumnChanges$.subscribe(t=>{t.forEach(n=>n.setIsFirstRight(n===t[0]))}),(0,ne.a)([this.nzTableStyleService.listOfListOfThWidth$,this.listOfFixedLeftColumnChanges$]).pipe((0,p.R)(this.destroy$)).subscribe(([t,n])=>{n.forEach((o,c)=>{if(o.isAutoLeft){const y=n.slice(0,c).reduce((N,B)=>N+(B.colspan||B.colSpan||1),0),w=t.slice(0,y).reduce((N,B)=>N+B,0);o.setAutoLeftWidth(`${w}px`)}})}),(0,ne.a)([this.nzTableStyleService.listOfListOfThWidth$,this.listOfFixedRightColumnChanges$]).pipe((0,p.R)(this.destroy$)).subscribe(([t,n])=>{n.forEach((o,c)=>{const u=n[n.length-c-1];if(u.isAutoRight){const w=n.slice(n.length-c,n.length).reduce((B,T)=>B+(T.colspan||T.colSpan||1),0),N=t.slice(t.length-w,t.length).reduce((B,T)=>B+T,0);u.setAutoRightWidth(`${N}px`)}})}))}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(de,8))},i.\u0275dir=e.lG2({type:i,selectors:[["tr",3,"mat-row","",3,"mat-header-row","",3,"nz-table-measure-row","",3,"nzExpand","",3,"nz-table-fixed-row",""]],contentQueries:function(t,n,o){if(1&t&&(e.Suo(o,Be,4),e.Suo(o,Ee,4)),2&t){let c;e.iGM(c=e.CRH())&&(n.listOfNzThDirective=c),e.iGM(c=e.CRH())&&(n.listOfCellFixedDirective=c)}},hostVars:2,hostBindings:function(t,n){2&t&&e.ekj("ant-table-row",n.isInsideTable)}}),a})(),Tn=(()=>{var i;class a{constructor(t,n,o,c){this.elementRef=t,this.renderer=n,this.nzTableStyleService=o,this.nzTableDataService=c,this.destroy$=new k.x,this.isInsideTable=!1,this.nzSortOrderChange=new e.vpe,this.isInsideTable=!!this.nzTableStyleService}ngOnInit(){this.nzTableStyleService&&this.nzTableStyleService.setTheadTemplate(this.templateRef)}ngAfterContentInit(){if(this.nzTableStyleService){const t=this.listOfNzTrDirective.changes.pipe((0,ie.O)(this.listOfNzTrDirective),(0,K.U)(u=>u&&u.first)),n=t.pipe((0,X.w)(u=>u?u.listOfColumnsChanges$:Te.E),(0,p.R)(this.destroy$));n.subscribe(u=>this.nzTableStyleService.setListOfTh(u)),this.nzTableStyleService.enableAutoMeasure$.pipe((0,X.w)(u=>u?n:(0,st.of)([]))).pipe((0,p.R)(this.destroy$)).subscribe(u=>this.nzTableStyleService.setListOfMeasureColumn(u));const o=t.pipe((0,X.w)(u=>u?u.listOfFixedLeftColumnChanges$:Te.E),(0,p.R)(this.destroy$)),c=t.pipe((0,X.w)(u=>u?u.listOfFixedRightColumnChanges$:Te.E),(0,p.R)(this.destroy$));o.subscribe(u=>{this.nzTableStyleService.setHasFixLeft(0!==u.length)}),c.subscribe(u=>{this.nzTableStyleService.setHasFixRight(0!==u.length)})}if(this.nzTableDataService){const t=this.listOfNzThAddOnComponent.changes.pipe((0,ie.O)(this.listOfNzThAddOnComponent));t.pipe((0,X.w)(()=>(0,pe.T)(...this.listOfNzThAddOnComponent.map(c=>c.manualClickOrder$))),(0,p.R)(this.destroy$)).subscribe(c=>{this.nzSortOrderChange.emit({key:c.nzColumnKey,value:c.sortOrder}),c.nzSortFn&&!1===c.nzSortPriority&&this.listOfNzThAddOnComponent.filter(y=>y!==c).forEach(y=>y.clearSortOrder())}),t.pipe((0,X.w)(c=>(0,pe.T)(t,...c.map(u=>u.calcOperatorChange$)).pipe((0,Oe.z)(()=>t))),(0,K.U)(c=>c.filter(u=>!!u.nzSortFn||!!u.nzFilterFn).map(u=>{const{nzSortFn:y,sortOrder:w,nzFilterFn:N,nzFilterValue:B,nzSortPriority:T,nzColumnKey:G}=u;return{key:G,sortFn:y,sortPriority:T,sortOrder:w,filterFn:N,filterValue:B}})),(0,be.g)(0),(0,p.R)(this.destroy$)).subscribe(c=>{this.nzTableDataService.listOfCalcOperator$.next(c)})}}ngAfterViewInit(){this.nzTableStyleService&&this.renderer.removeChild(this.renderer.parentNode(this.elementRef.nativeElement),this.elementRef.nativeElement)}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.SBq),e.Y36(e.Qsj),e.Y36(de,8),e.Y36(we,8))},i.\u0275cmp=e.Xpm({type:i,selectors:[["thead",9,"ant-table-thead"]],contentQueries:function(t,n,o){if(1&t&&(e.Suo(o,Je,5),e.Suo(o,Re,5)),2&t){let c;e.iGM(c=e.CRH())&&(n.listOfNzTrDirective=c),e.iGM(c=e.CRH())&&(n.listOfNzThAddOnComponent=c)}},viewQuery:function(t,n){if(1&t&&e.Gf(rn,7),2&t){let o;e.iGM(o=e.CRH())&&(n.templateRef=o.first)}},outputs:{nzSortOrderChange:"nzSortOrderChange"},ngContentSelectors:ge,decls:3,vars:1,consts:[["contentTemplate",""],[4,"ngIf"],[3,"ngTemplateOutlet"]],template:function(t,n){1&t&&(e.F$t(),e.YNc(0,cn,1,0,"ng-template",null,0,e.W1O),e.YNc(2,hn,2,1,"ng-container",1)),2&t&&(e.xp6(2),e.Q6J("ngIf",!n.isInsideTable))},dependencies:[S.O5,S.tP],encapsulation:2,changeDetection:0}),a})(),yn=(()=>{var i;class a{}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275mod=e.oAB({type:i}),i.\u0275inj=e.cJS({imports:[_.vT,ee.ip,g.u5,L.T,Pe.aF,J.Wr,Z.b1,Y.sL,S.ez,l.ud,it,Q.y7,Me.j,V.YI,U.PV,W.Xo,r.Cl]}),a})()}}]); \ No newline at end of file diff --git a/worklenz-backend/src/public/148.3562c20f7c79dfbe.js b/worklenz-backend/src/public/148.3562c20f7c79dfbe.js new file mode 100644 index 00000000..513898df --- /dev/null +++ b/worklenz-backend/src/public/148.3562c20f7c79dfbe.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkworklenz=self.webpackChunkworklenz||[]).push([[148],{17816:(zo,wi,ct)=>{ct.d(wi,{ZL:()=>Qt,kL:()=>zn,od:()=>Ki,qi:()=>we,zX:()=>rr});var R=ct(4942),u=ct(98137);class E{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(i,t,e,n){const r=t.duration;t.listeners[n].forEach(l=>l({chart:i,initial:t.initial,numSteps:r,currentStep:Math.min(e-t.start,r)}))}_refresh(){this._request||(this._running=!0,this._request=u.r.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(i=Date.now()){let t=0;this._charts.forEach((e,n)=>{if(!e.running||!e.items.length)return;const o=e.items;let c,r=o.length-1,l=!1;for(;r>=0;--r)c=o[r],c._active?(c._total>e.duration&&(e.duration=c._total),c.tick(i),l=!0):(o[r]=o[o.length-1],o.pop());l&&(n.draw(),this._notify(n,e,i,"progress")),o.length||(e.running=!1,this._notify(n,e,i,"complete"),e.initial=!1),t+=o.length}),this._lastDate=i,0===t&&(this._running=!1)}_getAnims(i){const t=this._charts;let e=t.get(i);return e||(e={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},t.set(i,e)),e}listen(i,t,e){this._getAnims(i).listeners[t].push(e)}add(i,t){!t||!t.length||this._getAnims(i).items.push(...t)}has(i){return this._getAnims(i).items.length>0}start(i){const t=this._charts.get(i);t&&(t.running=!0,t.start=Date.now(),t.duration=t.items.reduce((e,n)=>Math.max(e,n._duration),0),this._refresh())}running(i){if(!this._running)return!1;const t=this._charts.get(i);return!(!t||!t.running||!t.items.length)}stop(i){const t=this._charts.get(i);if(!t||!t.items.length)return;const e=t.items;let n=e.length-1;for(;n>=0;--n)e[n].cancel();t.items=[],this._notify(i,t,Date.now(),"complete")}remove(i){return this._charts.delete(i)}}var Ot=new E;const St="transparent",Dt={boolean:(s,i,t)=>t>.5?i:s,color(s,i,t){const e=(0,u.c)(s||St),n=e.valid&&(0,u.c)(i||St);return n&&n.valid?n.mix(e,t).hexString():i},number:(s,i,t)=>s+(i-s)*t};class Ve{constructor(i,t,e,n){const o=t[e];n=(0,u.a)([i.to,n,o,i.from]);const r=(0,u.a)([i.from,o,n]);this._active=!0,this._fn=i.fn||Dt[i.type||typeof r],this._easing=u.e[i.easing]||u.e.linear,this._start=Math.floor(Date.now()+(i.delay||0)),this._duration=this._total=Math.floor(i.duration),this._loop=!!i.loop,this._target=t,this._prop=e,this._from=r,this._to=n,this._promises=void 0}active(){return this._active}update(i,t,e){if(this._active){this._notify(!1);const n=this._target[this._prop],o=e-this._start,r=this._duration-o;this._start=e,this._duration=Math.floor(Math.max(r,i.duration)),this._total+=o,this._loop=!!i.loop,this._to=(0,u.a)([i.to,t,n,i.from]),this._from=(0,u.a)([i.from,n,t])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(i){const t=i-this._start,e=this._duration,n=this._prop,o=this._from,r=this._loop,l=this._to;let c;if(this._active=o!==l&&(r||t1?2-c:c,c=this._easing(Math.min(1,Math.max(0,c))),this._target[n]=this._fn(o,l,c))}wait(){const i=this._promises||(this._promises=[]);return new Promise((t,e)=>{i.push({res:t,rej:e})})}_notify(i){const t=i?"res":"rej",e=this._promises||[];for(let n=0;n{const o=i[n];if(!(0,u.i)(o))return;const r={};for(const l of t)r[l]=o[l];((0,u.b)(o.properties)&&o.properties||[n]).forEach(l=>{(l===n||!e.has(l))&&e.set(l,r)})})}_animateOptions(i,t){const e=t.options,n=function We(s,i){if(!i)return;let t=s.options;if(t)return t.$shared&&(s.options=t=Object.assign({},t,{$shared:!1,$animations:{}})),t;s.options=i}(i,e);if(!n)return[];const o=this._createAnimations(n,e);return e.$shared&&function Ut(s,i){const t=[],e=Object.keys(i);for(let n=0;n{i.options=e},()=>{}),o}_createAnimations(i,t){const e=this._properties,n=[],o=i.$animations||(i.$animations={}),r=Object.keys(t),l=Date.now();let c;for(c=r.length-1;c>=0;--c){const d=r[c];if("$"===d.charAt(0))continue;if("options"===d){n.push(...this._animateOptions(i,t));continue}const g=t[d];let m=o[d];const x=e.get(d);if(m){if(x&&m.active()){m.update(x,g,l);continue}m.cancel()}x&&x.duration?(o[d]=m=new Ve(x,i,d,g),n.push(m)):i[d]=g}return n}update(i,t){if(0===this._properties.size)return void Object.assign(i,t);const e=this._createAnimations(i,t);return e.length?(Ot.add(this._chart,e),!0):void 0}}function ie(s,i){const t=s&&s.options||{},e=t.reverse,n=void 0===t.min?i:0,o=void 0===t.max?i:0;return{start:e?o:n,end:e?n:o}}function Ye(s,i){const t=[],e=s._getSortedDatasetMetas(i);let n,o;for(n=0,o=e.length;n0||!t&&o<0)return n.index}return null}function At(s,i){const{chart:t,_cachedMeta:e}=s,n=t._stacks||(t._stacks={}),{iScale:o,vScale:r,index:l}=e,c=o.axis,d=r.axis,g=function Ze(s,i,t){return`${s.id}.${i.id}.${t.stack||t.type}`}(o,r,e),m=i.length;let x;for(let v=0;vt[e].axis===i).shift()}function ne(s,i){const t=s.controller.index,e=s.vScale&&s.vScale.axis;if(e){i=i||s._parsed;for(const n of i){const o=n._stacks;if(!o||void 0===o[e]||void 0===o[e][t])return;delete o[e][t],void 0!==o[e]._visualValues&&void 0!==o[e]._visualValues[t]&&delete o[e]._visualValues[t]}}}const se=s=>"reset"===s||"none"===s,fe=(s,i)=>i?s:Object.assign({},s);let It=(()=>{class s{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Ue(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&ne(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,n=this.getDataset(),o=(x,v,S,P)=>"x"===x?v:"r"===x?P:S,r=e.xAxisID=(0,u.v)(n.xAxisID,Tt(t,"x")),l=e.yAxisID=(0,u.v)(n.yAxisID,Tt(t,"y")),c=e.rAxisID=(0,u.v)(n.rAxisID,Tt(t,"r")),d=e.indexAxis,g=e.iAxisID=o(d,r,l,c),m=e.vAxisID=o(d,l,r,c);e.xScale=this.getScaleForId(r),e.yScale=this.getScaleForId(l),e.rScale=this.getScaleForId(c),e.iScale=this.getScaleForId(g),e.vScale=this.getScaleForId(m)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&(0,u.u)(this._data,this),t._stacked&&ne(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),n=this._data;if((0,u.i)(e))this._data=function Pi(s){const i=Object.keys(s),t=new Array(i.length);let e,n,o;for(e=0,n=i.length;e0&&n._parsed[t-1];if(!1===this._parsing)n._parsed=o,n._sorted=!0,v=o;else{v=(0,u.b)(o[t])?this.parseArrayData(n,o,t,e):(0,u.i)(o[t])?this.parseObjectData(n,o,t,e):this.parsePrimitiveData(n,o,t,e);const S=()=>null===x[c]||g&&x[c]s&&!i.hidden&&i._stacked&&{keys:Ye(this.chart,!0),values:null})(e,n),g={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:m,max:x}=function Ci(s){const{min:i,max:t,minDefined:e,maxDefined:n}=s.getUserBounds();return{min:e?i:Number.NEGATIVE_INFINITY,max:n?t:Number.POSITIVE_INFINITY}}(c);let v,S;function P(){S=o[v];const O=S[c.axis];return!(0,u.g)(S[t.axis])||m>O||x=0;--v)if(!P()){this.updateRangeFromParsed(g,t,S,d);break}return g}getAllParsedValues(t){const e=this._cachedMeta._parsed,n=[];let o,r,l;for(o=0,r=e.length;o=0&&tthis.getContext(n,o,e),x);return O.$shared&&(O.$shared=d,r[l]=Object.freeze(fe(O,d))),O}_resolveAnimations(t,e,n){const o=this.chart,r=this._cachedDataOpts,l=`animation-${e}`,c=r[l];if(c)return c;let d;if(!1!==o.options.animation){const m=this.chart.config,x=m.datasetAnimationScopeKeys(this._type,e),v=m.getOptionScopes(this.getDataset(),x);d=m.createResolver(v,this.getContext(t,n,e))}const g=new mt(o,d&&d.animations);return d&&d._cacheable&&(r[l]=Object.freeze(g)),g}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||se(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const n=this.resolveDataElementOptions(t,e),o=this._sharedOptions,r=this.getSharedOptions(n),l=this.includeOptions(e,r)||r!==o;return this.updateSharedOptions(r,e,n),{sharedOptions:r,includeOptions:l}}updateElement(t,e,n,o){se(o)?Object.assign(t,n):this._resolveAnimations(e,o).update(t,n)}updateSharedOptions(t,e,n){t&&!se(e)&&this._resolveAnimations(void 0,e).update(t,n)}_setStyle(t,e,n,o){t.active=o;const r=this.getStyle(e,o);this._resolveAnimations(e,n,o).update(t,{options:!o&&this.getSharedOptions(r)||r})}removeHoverStyle(t,e,n){this._setStyle(t,n,"active",!1)}setHoverStyle(t,e,n){this._setStyle(t,n,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,n=this._cachedMeta.data;for(const[c,d,g]of this._syncList)this[c](d,g);this._syncList=[];const o=n.length,r=e.length,l=Math.min(r,o);l&&this.parse(0,l),r>o?this._insertElements(o,r-o,t):r{for(g.length+=e,c=g.length-1;c>=l;c--)g[c]=g[c-e]};for(d(r),c=t;cn-o))}return s._cache.$bar}(i,s.type);let n,o,r,l,e=i._length;const c=()=>{32767===r||-32768===r||((0,u.h)(l)&&(e=Math.min(e,Math.abs(r-l)||e)),l=r)};for(n=0,o=t.length;nMath.abs(l)&&(c=l,d=r),i[t.axis]=d,i._custom={barStart:c,barEnd:d,start:n,end:o,min:r,max:l}}(s,i,t,e):i[t.axis]=t.parse(s,e),i}function q(s,i,t,e){const n=s.iScale,o=s.vScale,r=n.getLabels(),l=n===o,c=[];let d,g,m,x;for(d=t,g=t+e;ds.x,t="left",e="right"):(i=s.base{class s extends It{parsePrimitiveData(t,e,n,o){return q(t,e,n,o)}parseArrayData(t,e,n,o){return q(t,e,n,o)}parseObjectData(t,e,n,o){const{iScale:r,vScale:l}=t,{xAxisKey:c="x",yAxisKey:d="y"}=this._parsing,g="x"===r.axis?c:d,m="x"===l.axis?c:d,x=[];let v,S,P,O;for(v=n,S=n+o;vd.controller.options.grouped),r=n.options.stacked,l=[],c=d=>{const g=d.controller.getParsed(e),m=g&&g[d.vScale.axis];if((0,u.k)(m)||isNaN(m))return!0};for(const d of o)if((void 0===e||!c(d))&&((!1===r||-1===l.indexOf(d.stack)||void 0===r&&void 0===d.stack)&&l.push(d.stack),d.index===t))break;return l.length||l.push(void 0),l}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,n){const o=this._getStacks(t,n),r=void 0!==e?o.indexOf(e):-1;return-1===r?o.length-1:r}_getRuler(){const t=this.options,e=this._cachedMeta,n=e.iScale,o=[];let r,l;for(r=0,l=e.data.length;r=t?1:-1)}(O,e,c)*l,x===c&&(A-=O/2);const B=e.getPixelForDecimal(0),j=e.getPixelForDecimal(1),L=Math.min(B,j),H=Math.max(B,j);A=Math.max(Math.min(A,H),L),P=A+O,n&&!m&&(d._stacks[e.axis]._visualValues[o]=e.getValueForPixel(P)-e.getValueForPixel(A))}if(A===e.getPixelForValue(c)){const B=(0,u.s)(O)*e.getLineWidthForValue(c)/2;A+=B,O-=B}return{size:O,base:A,head:P,center:P+O/2}}_calculateBarIndexPixels(t,e){const n=e.scale,o=this.options,r=o.skipNull,l=(0,u.v)(o.maxBarThickness,1/0);let c,d;if(e.grouped){const g=r?this._getStackCount(t):e.stackCount,m="flex"===o.barThickness?function ge(s,i,t,e){const n=i.pixels,o=n[s];let r=s>0?n[s-1]:null,l=s{class s extends It{initialize(){this.enableOptionSharing=!0,super.initialize()}parsePrimitiveData(t,e,n,o){const r=super.parsePrimitiveData(t,e,n,o);for(let l=0;l=0;--n)e=Math.max(e,t[n].size(this.resolveDataElementOptions(n))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,n=this.chart.data.labels||[],{xScale:o,yScale:r}=e,l=this.getParsed(t),c=o.getLabelForValue(l.x),d=r.getLabelForValue(l.y),g=l._custom;return{label:n[t]||"",value:"("+c+", "+d+(g?", "+g:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,n,o){const r="reset"===o,{iScale:l,vScale:c}=this._cachedMeta,{sharedOptions:d,includeOptions:g}=this._getSharedOptions(e,o),m=l.axis,x=c.axis;for(let v=e;v{class s extends It{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const n=this.getDataset().data,o=this._cachedMeta;if(!1===this._parsing)o._parsed=n;else{let l,c,r=d=>+n[d];if((0,u.i)(n[t])){const{key:d="value"}=this._parsing;r=g=>+(0,u.f)(n[g],d)}for(l=t,c=t+e;l(0,u.p)(B,l,c,!0)?1:Math.max(j,j*t,L,L*t),S=(B,j,L)=>(0,u.p)(B,l,c,!0)?-1:Math.min(j,j*t,L,L*t),P=v(0,d,m),O=v(u.H,g,x),z=S(u.P,d,m),A=S(u.P+u.H,g,x);e=(P-z)/2,n=(O-A)/2,o=-(P+z)/2,r=-(O+A)/2}return{ratioX:e,ratioY:n,offsetX:o,offsetY:r}}(x,m,d),B=Math.max(Math.min((n.width-l)/v,(n.height-l)/S)/2,0),j=(0,u.n)(this.options.radius,B),H=(j-Math.max(j*d,0))/this._getVisibleDatasetWeightTotal();this.offsetX=P*j,this.offsetY=O*j,o.total=this.calculateTotal(),this.outerRadius=j-H*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-H*g,0),this.updateElements(r,0,r.length,t)}_circumference(t,e){const n=this.options,o=this._cachedMeta,r=this._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]||o.data[t].hidden?0:this.calculateCircumference(o._parsed[t]*r/u.T)}updateElements(t,e,n,o){const r="reset"===o,l=this.chart,c=l.chartArea,m=(c.left+c.right)/2,x=(c.top+c.bottom)/2,v=r&&l.options.animation.animateScale,S=v?0:this.innerRadius,P=v?0:this.outerRadius,{sharedOptions:O,includeOptions:z}=this._getSharedOptions(e,o);let B,A=this._getRotation();for(B=0;B0&&!isNaN(t)?u.T*(Math.abs(t)/e):0}getLabelAndValue(t){const n=this.chart,o=n.data.labels||[],r=(0,u.o)(this._cachedMeta._parsed[t],n.options.locale);return{label:o[t]||"",value:r}}getMaxBorderWidth(t){let e=0;const n=this.chart;let o,r,l,c,d;if(!t)for(o=0,r=n.data.datasets.length;o"spacing"!==i,_indexable:i=>"spacing"!==i&&!i.startsWith("borderDash")&&!i.startsWith("hoverBorderDash")}),(0,R.Z)(s,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(i){const t=i.data;if(t.labels.length&&t.datasets.length){const{labels:{pointStyle:e,color:n}}=i.legend.options;return t.labels.map((o,r)=>{const c=i.getDatasetMeta(0).controller.getStyle(r);return{text:o,fillStyle:c.backgroundColor,strokeStyle:c.borderColor,fontColor:n,lineWidth:c.borderWidth,pointStyle:e,hidden:!i.getDataVisibility(r),index:r}})}return[]}},onClick(i,t,e){e.chart.toggleDataVisibility(t.index),e.chart.update()}}}}),s})(),Oe=(()=>{class s extends It{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(t){const e=this._cachedMeta,{dataset:n,data:o=[],_dataset:r}=e,l=this.chart._animationsDisabled;let{start:c,count:d}=(0,u.q)(e,o,l);this._drawStart=c,this._drawCount=d,(0,u.w)(e)&&(c=0,d=o.length),n._chart=this.chart,n._datasetIndex=this.index,n._decimated=!!r._decimated,n.points=o;const g=this.resolveDatasetElementOptions(t);this.options.showLine||(g.borderWidth=0),g.segment=this.options.segment,this.updateElement(n,void 0,{animated:!l,options:g},t),this.updateElements(o,c,d,t)}updateElements(t,e,n,o){const r="reset"===o,{iScale:l,vScale:c,_stacked:d,_dataset:g}=this._cachedMeta,{sharedOptions:m,includeOptions:x}=this._getSharedOptions(e,o),v=l.axis,S=c.axis,{spanGaps:P,segment:O}=this.options,z=(0,u.x)(P)?P:Number.POSITIVE_INFINITY,A=this.chart._animationsDisabled||r||"none"===o,B=e+n,j=t.length;let L=e>0&&this.getParsed(e-1);for(let H=0;H=B){V.skip=!0;continue}const U=this.getParsed(H),tt=(0,u.k)(U[S]),J=V[v]=l.getPixelForValue(U[v],H),nt=V[S]=r||tt?c.getBasePixel():c.getPixelForValue(d?this.applyStack(c,U,d):U[S],H);V.skip=isNaN(J)||isNaN(nt)||tt,V.stop=H>0&&Math.abs(U[v]-L[v])>z,O&&(V.parsed=U,V.raw=g.data[H]),x&&(V.options=m||this.resolveDataElementOptions(H,W.active?"active":o)),A||this.updateElement(W,H,V,o),L=U}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,n=e.options&&e.options.borderWidth||0,o=t.data||[];if(!o.length)return n;const r=o[0].size(this.resolveDataElementOptions(0)),l=o[o.length-1].size(this.resolveDataElementOptions(o.length-1));return Math.max(n,r,l)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}}return(0,R.Z)(s,"id","line"),(0,R.Z)(s,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),(0,R.Z)(s,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}}),s})(),_t=(()=>{class s extends It{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const n=this.chart,o=n.data.labels||[],r=(0,u.o)(this._cachedMeta._parsed[t].r,n.options.locale);return{label:o[t]||"",value:r}}parseObjectData(t,e,n,o){return u.y.bind(this)(t,e,n,o)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return this._cachedMeta.data.forEach((n,o)=>{const r=this.getParsed(o).r;!isNaN(r)&&this.chart.getDataVisibility(o)&&(re.max&&(e.max=r))}),e}_updateRadius(){const t=this.chart,e=t.chartArea,n=t.options,o=Math.min(e.right-e.left,e.bottom-e.top),r=Math.max(o/2,0),c=(r-Math.max(n.cutoutPercentage?r/100*n.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=r-c*this.index,this.innerRadius=this.outerRadius-c}updateElements(t,e,n,o){const r="reset"===o,l=this.chart,d=l.options.animation,g=this._cachedMeta.rScale,m=g.xCenter,x=g.yCenter,v=g.getIndexAngle(0)-.5*u.P;let P,S=v;const O=360/this.countVisibleElements();for(P=0;P{!isNaN(this.getParsed(o).r)&&this.chart.getDataVisibility(o)&&e++}),e}_computeAngle(t,e,n){return this.chart.getDataVisibility(t)?(0,u.t)(this.resolveDataElementOptions(t,e).angle||n):0}}return(0,R.Z)(s,"id","polarArea"),(0,R.Z)(s,"defaults",{dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0}),(0,R.Z)(s,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(i){const t=i.data;if(t.labels.length&&t.datasets.length){const{labels:{pointStyle:e,color:n}}=i.legend.options;return t.labels.map((o,r)=>{const c=i.getDatasetMeta(0).controller.getStyle(r);return{text:o,fillStyle:c.backgroundColor,strokeStyle:c.borderColor,fontColor:n,lineWidth:c.borderWidth,pointStyle:e,hidden:!i.getDataVisibility(r),index:r}})}return[]}},onClick(i,t,e){e.chart.toggleDataVisibility(t.index),e.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}}),s})();var yt=Object.freeze({__proto__:null,BarController:pe,BubbleController:ho,DoughnutController:zi,LineController:Oe,PieController:(()=>{class s extends zi{}return(0,R.Z)(s,"id","pie"),(0,R.Z)(s,"defaults",{cutout:0,rotation:0,circumference:360,radius:"100%"}),s})(),PolarAreaController:_t,RadarController:(()=>{class s extends It{getLabelAndValue(t){const e=this._cachedMeta.vScale,n=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(n[e.axis])}}parseObjectData(t,e,n,o){return u.y.bind(this)(t,e,n,o)}update(t){const e=this._cachedMeta,n=e.dataset,o=e.data||[],r=e.iScale.getLabels();if(n.points=o,"resize"!==t){const l=this.resolveDatasetElementOptions(t);this.options.showLine||(l.borderWidth=0),this.updateElement(n,void 0,{_loop:!0,_fullLoop:r.length===o.length,options:l},t)}this.updateElements(o,0,o.length,t)}updateElements(t,e,n,o){const r=this._cachedMeta.rScale,l="reset"===o;for(let c=e;c{class s extends It{getLabelAndValue(t){const e=this._cachedMeta,n=this.chart.data.labels||[],{xScale:o,yScale:r}=e,l=this.getParsed(t),c=o.getLabelForValue(l.x),d=r.getLabelForValue(l.y);return{label:n[t]||"",value:"("+c+", "+d+")"}}update(t){const e=this._cachedMeta,{data:n=[]}=e,o=this.chart._animationsDisabled;let{start:r,count:l}=(0,u.q)(e,n,o);if(this._drawStart=r,this._drawCount=l,(0,u.w)(e)&&(r=0,l=n.length),this.options.showLine){this.datasetElementType||this.addElements();const{dataset:c,_dataset:d}=e;c._chart=this.chart,c._datasetIndex=this.index,c._decimated=!!d._decimated,c.points=n;const g=this.resolveDatasetElementOptions(t);g.segment=this.options.segment,this.updateElement(c,void 0,{animated:!o,options:g},t)}else this.datasetElementType&&(delete e.dataset,this.datasetElementType=!1);this.updateElements(n,r,l,t)}addElements(){const{showLine:t}=this.options;!this.datasetElementType&&t&&(this.datasetElementType=this.chart.registry.getElement("line")),super.addElements()}updateElements(t,e,n,o){const r="reset"===o,{iScale:l,vScale:c,_stacked:d,_dataset:g}=this._cachedMeta,m=this.resolveDataElementOptions(e,o),x=this.getSharedOptions(m),v=this.includeOptions(o,x),S=l.axis,P=c.axis,{spanGaps:O,segment:z}=this.options,A=(0,u.x)(O)?O:Number.POSITIVE_INFINITY,B=this.chart._animationsDisabled||r||"none"===o;let j=e>0&&this.getParsed(e-1);for(let L=e;L0&&Math.abs(W[S]-j[S])>A,z&&(V.parsed=W,V.raw=g.data[L]),v&&(V.options=x||this.resolveDataElementOptions(L,H.active?"active":o)),B||this.updateElement(H,L,V,o),j=W}this.updateSharedOptions(x,o,m)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let c=0;for(let d=e.length-1;d>=0;--d)c=Math.max(c,e[d].size(this.resolveDataElementOptions(d))/2);return c>0&&c}const n=t.dataset,o=n.options&&n.options.borderWidth||0;if(!e.length)return o;const r=e[0].size(this.resolveDataElementOptions(0)),l=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(o,r,l)/2}}return(0,R.Z)(s,"id","scatter"),(0,R.Z)(s,"defaults",{datasetElementType:!1,dataElementType:"point",showLine:!1,fill:!1}),(0,R.Z)(s,"overrides",{interaction:{mode:"point"},scales:{x:{type:"linear"},y:{type:"linear"}}}),s})()});function oe(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class Je{static override(i){Object.assign(Je.prototype,i)}constructor(i){(0,R.Z)(this,"options",void 0),this.options=i||{}}init(){}formats(){return oe()}parse(){return oe()}format(){return oe()}add(){return oe()}diff(){return oe()}startOf(){return oe()}endOf(){return oe()}}var ls__date=Je;function cs(s,i,t,e){const{controller:n,data:o,_sorted:r}=s,l=n._cachedMeta.iScale;if(l&&i===l.axis&&"r"!==i&&r&&o.length){const c=l._reversePixels?u.A:u.B;if(!e)return c(o,i,t);if(n._sharedOptions){const d=o[0],g="function"==typeof d.getRange&&d.getRange(i);if(g){const m=c(o,i,t-g),x=c(o,i,t+g);return{lo:m.lo,hi:x.hi}}}}return{lo:0,hi:o.length-1}}function Ae(s,i,t,e,n){const o=s.getSortedVisibleDatasetMetas(),r=t[i];for(let l=0,c=o.length;l{c[r](i[t],n)&&(o.push({element:c,datasetIndex:d,index:g}),l=l||c.inRange(i.x,i.y,n))}),e&&!l?[]:o}var fo={evaluateInteractionItems:Ae,modes:{index(s,i,t,e){const n=(0,u.z)(i,s),o=t.axis||"x",r=t.includeInvisible||!1,l=t.intersect?ti(s,n,o,e,r):ei(s,n,o,!1,e,r),c=[];return l.length?(s.getSortedVisibleDatasetMetas().forEach(d=>{const g=l[0].index,m=d.data[g];m&&!m.skip&&c.push({element:m,datasetIndex:d.index,index:g})}),c):[]},dataset(s,i,t,e){const n=(0,u.z)(i,s),o=t.axis||"xy",r=t.includeInvisible||!1;let l=t.intersect?ti(s,n,o,e,r):ei(s,n,o,!1,e,r);if(l.length>0){const c=l[0].datasetIndex,d=s.getDatasetMeta(c).data;l=[];for(let g=0;gti(s,(0,u.z)(i,s),t.axis||"xy",e,t.includeInvisible||!1),nearest:(s,i,t,e)=>ei(s,(0,u.z)(i,s),t.axis||"xy",t.intersect,e,t.includeInvisible||!1),x:(s,i,t,e)=>rn(s,(0,u.z)(i,s),"x",t.intersect,e),y:(s,i,t,e)=>rn(s,(0,u.z)(i,s),"y",t.intersect,e)}};const Ei=["left","top","right","bottom"];function Te(s,i){return s.filter(t=>t.pos===i)}function an(s,i){return s.filter(t=>-1===Ei.indexOf(t.pos)&&t.box.axis===i)}function re(s,i){return s.sort((t,e)=>{const n=i?e:t,o=i?t:e;return n.weight===o.weight?n.index-o.index:n.weight-o.weight})}function ln(s,i,t,e){return Math.max(s[t],i[t])+Math.max(s[e],i[e])}function ft(s,i){s.top=Math.max(s.top,i.top),s.left=Math.max(s.left,i.left),s.bottom=Math.max(s.bottom,i.bottom),s.right=Math.max(s.right,i.right)}function Et(s,i,t,e){const{pos:n,box:o}=t,r=s.maxPadding;if(!(0,u.i)(n)){t.size&&(s[n]-=t.size);const m=e[t.stack]||{size:0,count:1};m.size=Math.max(m.size,t.horizontal?o.height:o.width),t.size=m.size/m.count,s[n]+=t.size}o.getPadding&&ft(r,o.getPadding());const l=Math.max(0,i.outerWidth-ln(r,s,"left","right")),c=Math.max(0,i.outerHeight-ln(r,s,"top","bottom")),d=l!==s.w,g=c!==s.h;return s.w=l,s.h=c,t.horizontal?{same:d,other:g}:{same:g,other:d}}function ii(s,i){const t=i.maxPadding;return function e(n){const o={left:0,top:0,right:0,bottom:0};return n.forEach(r=>{o[r]=Math.max(i[r],t[r])}),o}(s?["left","right"]:["top","bottom"])}function Le(s,i,t,e){const n=[];let o,r,l,c,d,g;for(o=0,r=s.length,d=0;od.box.fullSize),!0),e=re(Te(i,"left"),!0),n=re(Te(i,"right")),o=re(Te(i,"top"),!0),r=re(Te(i,"bottom")),l=an(i,"x"),c=an(i,"y");return{fullSize:t,leftAndTop:e.concat(o),rightAndBottom:n.concat(c).concat(r).concat(l),chartArea:Te(i,"chartArea"),vertical:e.concat(n).concat(c),horizontal:o.concat(r).concat(l)}}(s.boxes),c=l.vertical,d=l.horizontal;(0,u.F)(s.boxes,P=>{"function"==typeof P.beforeLayout&&P.beforeLayout()});const g=c.reduce((P,O)=>O.box.options&&!1===O.box.options.display?P:P+1,0)||1,m=Object.freeze({outerWidth:i,outerHeight:t,padding:n,availableWidth:o,availableHeight:r,vBoxMaxWidth:o/2/g,hBoxMaxHeight:r/2}),x=Object.assign({},n);ft(x,(0,u.E)(e));const v=Object.assign({maxPadding:x,w:o,h:r,x:n.left,y:n.top},n),S=function ze(s,i){const t=function hs(s){const i={};for(const t of s){const{stack:e,pos:n,stackWeight:o}=t;if(!e||!Ei.includes(n))continue;const r=i[e]||(i[e]={count:0,placed:0,weight:0,size:0});r.count++,r.weight+=o}return i}(s),{vBoxMaxWidth:e,hBoxMaxHeight:n}=i;let o,r,l;for(o=0,r=s.length;o{const O=P.box;Object.assign(O,s.chartArea),O.update(v.w,v.h,{left:0,top:0,right:0,bottom:0})})}};class _e{acquireContext(i,t){}releaseContext(i){return!1}addEventListener(i,t,e){}removeEventListener(i,t,e){}getDevicePixelRatio(){return 1}getMaximumSize(i,t,e,n){return t=Math.max(0,t||i.width),e=e||i.height,{width:t,height:Math.max(0,n?Math.floor(t/n):e)}}isAttached(i){return!0}updateConfig(i){}}class Ii extends _e{acquireContext(i){return i&&i.getContext&&i.getContext("2d")||null}updateConfig(i){i.options.animation=!1}}const Bt="$chartjs",fs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},cn=s=>null===s||""===s,hn=!!u.K&&{passive:!0};function ms(s,i,t){s.canvas.removeEventListener(i,t,hn)}function ni(s,i){for(const t of s)if(t===i||t.contains(i))return!0}function _s(s,i,t){const e=s.canvas,n=new MutationObserver(o=>{let r=!1;for(const l of o)r=r||ni(l.addedNodes,e),r=r&&!ni(l.removedNodes,e);r&&t()});return n.observe(document,{childList:!0,subtree:!0}),n}function Fi(s,i,t){const e=s.canvas,n=new MutationObserver(o=>{let r=!1;for(const l of o)r=r||ni(l.removedNodes,e),r=r&&!ni(l.addedNodes,e);r&&t()});return n.observe(document,{childList:!0,subtree:!0}),n}const Ee=new Map;let Ht=0;function Bi(){const s=window.devicePixelRatio;s!==Ht&&(Ht=s,Ee.forEach((i,t)=>{t.currentDevicePixelRatio!==s&&i()}))}function dn(s,i,t){const e=s.canvas,n=e&&(0,u.I)(e);if(!n)return;const o=(0,u.L)((l,c)=>{const d=n.clientWidth;t(l,c),d{const c=l[0],d=c.contentRect.width,g=c.contentRect.height;0===d&&0===g||o(d,g)});return r.observe(n),function si(s,i){Ee.size||window.addEventListener("resize",Bi),Ee.set(s,i)}(s,o),r}function jt(s,i,t){t&&t.disconnect(),"resize"===i&&function xs(s){Ee.delete(s),Ee.size||window.removeEventListener("resize",Bi)}(s)}function xe(s,i,t){const e=s.canvas,n=(0,u.L)(o=>{null!==s.ctx&&t(function bs(s,i){const t=fs[s.type]||s.type,{x:e,y:n}=(0,u.z)(s,i);return{type:t,chart:i,native:s,x:void 0!==e?e:null,y:void 0!==n?n:null}}(o,s))},s);return function ps(s,i,t){s.addEventListener(i,t,hn)}(e,i,n),n}class ys extends _e{acquireContext(i,t){const e=i&&i.getContext&&i.getContext("2d");return e&&e.canvas===i?(function gs(s,i){const t=s.style,e=s.getAttribute("height"),n=s.getAttribute("width");if(s[Bt]={initial:{height:e,width:n,style:{display:t.display,height:t.height,width:t.width}}},t.display=t.display||"block",t.boxSizing=t.boxSizing||"border-box",cn(n)){const o=(0,u.J)(s,"width");void 0!==o&&(s.width=o)}if(cn(e))if(""===s.style.height)s.height=s.width/(i||2);else{const o=(0,u.J)(s,"height");void 0!==o&&(s.height=o)}}(i,t),e):null}releaseContext(i){const t=i.canvas;if(!t[Bt])return!1;const e=t[Bt].initial;["height","width"].forEach(o=>{const r=e[o];(0,u.k)(r)?t.removeAttribute(o):t.setAttribute(o,r)});const n=e.style||{};return Object.keys(n).forEach(o=>{t.style[o]=n[o]}),t.width=t.width,delete t[Bt],!0}addEventListener(i,t,e){this.removeEventListener(i,t),(i.$proxies||(i.$proxies={}))[t]=({attach:_s,detach:Fi,resize:dn}[t]||xe)(i,t,e)}removeEventListener(i,t){const e=i.$proxies||(i.$proxies={}),n=e[t];n&&(({attach:jt,detach:jt,resize:jt}[t]||ms)(i,t,n),e[t]=void 0)}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(i,t,e,n){return(0,u.G)(i,t,e,n)}isAttached(i){const t=(0,u.I)(i);return!(!t||!t.isConnected)}}class wt{constructor(){(0,R.Z)(this,"x",void 0),(0,R.Z)(this,"y",void 0),(0,R.Z)(this,"active",!1),(0,R.Z)(this,"options",void 0),(0,R.Z)(this,"$animations",void 0)}tooltipPosition(i){const{x:t,y:e}=this.getProps(["x","y"],i);return{x:t,y:e}}hasValue(){return(0,u.x)(this.x)&&(0,u.x)(this.y)}getProps(i,t){const e=this.$animations;if(!t||!e)return this;const n={};return i.forEach(o=>{n[o]=e[o]&&e[o].active()?e[o]._to:this[o]}),n}}function ri(s,i,t,e,n){const o=(0,u.v)(e,0),r=Math.min((0,u.v)(n,s.length),s.length);let c,d,g,l=0;for(t=Math.ceil(t),n&&(c=n-e,t=c/Math.floor(c/t)),g=o;g<0;)l++,g=Math.round(o+l*t);for(d=Math.max(o,0);d"top"===i||"left"===i?s[i]+t:s[i]-t,gn=(s,i)=>Math.min(i||s,s);function pn(s,i){const t=[],e=s.length/i,n=s.length;let o=0;for(;or+l)))return c}function ve(s){return s.drawTicks?s.tickLength:0}function Hi(s,i){if(!s.display)return 0;const t=(0,u.a0)(s.font,i),e=(0,u.E)(s.padding);return((0,u.b)(s.text)?s.text.length:1)*t.lineHeight+e.height}function Ps(s,i,t){let e=(0,u.a1)(s);return(t&&"right"!==i||!t&&"right"===i)&&(e=(s=>"left"===s?"right":"right"===s?"left":s)(e)),e}class Nt extends wt{constructor(i){super(),this.id=i.id,this.type=i.type,this.options=void 0,this.ctx=i.ctx,this.chart=i.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(i){this.options=i.setContext(this.getContext()),this.axis=i.axis,this._userMin=this.parse(i.min),this._userMax=this.parse(i.max),this._suggestedMin=this.parse(i.suggestedMin),this._suggestedMax=this.parse(i.suggestedMax)}parse(i,t){return i}getUserBounds(){let{_userMin:i,_userMax:t,_suggestedMin:e,_suggestedMax:n}=this;return i=(0,u.O)(i,Number.POSITIVE_INFINITY),t=(0,u.O)(t,Number.NEGATIVE_INFINITY),e=(0,u.O)(e,Number.POSITIVE_INFINITY),n=(0,u.O)(n,Number.NEGATIVE_INFINITY),{min:(0,u.O)(i,e),max:(0,u.O)(t,n),minDefined:(0,u.g)(i),maxDefined:(0,u.g)(t)}}getMinMax(i){let r,{min:t,max:e,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:t,max:e};const l=this.getMatchingVisibleMetas();for(let c=0,d=l.length;ce?e:t,e=n&&t>e?t:e,{min:(0,u.O)(t,(0,u.O)(e,t)),max:(0,u.O)(e,(0,u.O)(t,e))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const i=this.chart.data;return this.options.labels||(this.isHorizontal()?i.xLabels:i.yLabels)||i.labels||[]}getLabelItems(i=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(i))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){(0,u.Q)(this.options.beforeUpdate,[this])}update(i,t,e){const{beginAtZero:n,grace:o,ticks:r}=this.options,l=r.sampleSize;this.beforeUpdate(),this.maxWidth=i,this.maxHeight=t,this._margins=e=Object.assign({left:0,right:0,top:0,bottom:0},e),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+e.left+e.right:this.height+e.top+e.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=(0,u.R)(this,o,n),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const c=ln)return function un(s,i,t,e){let r,n=0,o=t[0];for(e=Math.ceil(e),r=0;rn)return c}return Math.max(n,1)}(o,i,n);if(r>0){let m,x;const v=r>1?Math.round((c-l)/(r-1)):null;for(ri(i,d,g,(0,u.k)(v)?0:l-v,l),m=0,x=r-1;m=o||e<=1||!this.isHorizontal())return void(this.labelRotation=n);const g=this._getLabelSizes(),m=g.widest.width,x=g.highest.height,v=(0,u.S)(this.chart.width-m,0,this.maxWidth);l=i.offset?this.maxWidth/e:v/(e-1),m+6>l&&(l=v/(e-(i.offset?.5:1)),c=this.maxHeight-ve(i.grid)-t.padding-Hi(i.title,this.chart.options.font),d=Math.sqrt(m*m+x*x),r=(0,u.U)(Math.min(Math.asin((0,u.S)((g.highest.height+6)/l,-1,1)),Math.asin((0,u.S)(c/d,-1,1))-Math.asin((0,u.S)(x/d,-1,1)))),r=Math.max(n,Math.min(o,r))),this.labelRotation=r}afterCalculateLabelRotation(){(0,u.Q)(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){(0,u.Q)(this.options.beforeFit,[this])}fit(){const i={width:0,height:0},{chart:t,options:{ticks:e,title:n,grid:o}}=this,r=this._isVisible(),l=this.isHorizontal();if(r){const c=Hi(n,t.options.font);if(l?(i.width=this.maxWidth,i.height=ve(o)+c):(i.height=this.maxHeight,i.width=ve(o)+c),e.display&&this.ticks.length){const{first:d,last:g,widest:m,highest:x}=this._getLabelSizes(),v=2*e.padding,S=(0,u.t)(this.labelRotation),P=Math.cos(S),O=Math.sin(S);l?i.height=Math.min(this.maxHeight,i.height+(e.mirror?0:O*m.width+P*x.height)+v):i.width=Math.min(this.maxWidth,i.width+(e.mirror?0:P*m.width+O*x.height)+v),this._calculatePadding(d,g,O,P)}}this._handleMargins(),l?(this.width=this._length=t.width-this._margins.left-this._margins.right,this.height=i.height):(this.width=i.width,this.height=this._length=t.height-this._margins.top-this._margins.bottom)}_calculatePadding(i,t,e,n){const{ticks:{align:o,padding:r},position:l}=this.options,c=0!==this.labelRotation,d="top"!==l&&"x"===this.axis;if(this.isHorizontal()){const g=this.getPixelForTick(0)-this.left,m=this.right-this.getPixelForTick(this.ticks.length-1);let x=0,v=0;c?d?(x=n*i.width,v=e*t.height):(x=e*i.height,v=n*t.width):"start"===o?v=t.width:"end"===o?x=i.width:"inner"!==o&&(x=i.width/2,v=t.width/2),this.paddingLeft=Math.max((x-g+r)*this.width/(this.width-g),0),this.paddingRight=Math.max((v-m+r)*this.width/(this.width-m),0)}else{let g=t.height/2,m=i.height/2;"start"===o?(g=0,m=i.height):"end"===o&&(g=t.height,m=0),this.paddingTop=g+r,this.paddingBottom=m+r}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){(0,u.Q)(this.options.afterFit,[this])}isHorizontal(){const{axis:i,position:t}=this.options;return"top"===t||"bottom"===t||"x"===i}isFullSize(){return this.options.fullSize}_convertTicksToLabels(i){let t,e;for(this.beforeTickToLabelConversion(),this.generateTickLabels(i),t=0,e=i.length;t{const e=t.gc,n=e.length/2;let o;if(n>i){for(o=0;o({width:r[U]||0,height:l[U]||0});return{first:V(0),last:V(t-1),widest:V(H),highest:V(W),widths:r,heights:l}}getLabelForValue(i){return i}getPixelForValue(i,t){return NaN}getValueForPixel(i){}getPixelForTick(i){const t=this.ticks;return i<0||i>t.length-1?null:this.getPixelForValue(t[i].value)}getPixelForDecimal(i){this._reversePixels&&(i=1-i);const t=this._startPixel+i*this._length;return(0,u.W)(this._alignToPixels?(0,u.X)(this.chart,t,0):t)}getDecimalForPixel(i){const t=(i-this._startPixel)/this._length;return this._reversePixels?1-t:t}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:i,max:t}=this;return i<0&&t<0?t:i>0&&t>0?i:0}getContext(i){const t=this.ticks||[];if(i>=0&&il*n?l/e:c/n:c*n0}_computeGridLineItems(i){const t=this.axis,e=this.chart,n=this.options,{grid:o,position:r,border:l}=n,c=o.offset,d=this.isHorizontal(),m=this.ticks.length+(c?1:0),x=ve(o),v=[],S=l.setContext(this.getContext()),P=S.display?S.width:0,O=P/2,z=function(ut){return(0,u.X)(e,ut,P)};let A,B,j,L,H,W,V,U,tt,J,nt,Mt;if("top"===r)A=z(this.bottom),W=this.bottom-x,U=A-O,J=z(i.top)+O,Mt=i.bottom;else if("bottom"===r)A=z(this.top),J=i.top,Mt=z(i.bottom)-O,W=A+O,U=this.top+x;else if("left"===r)A=z(this.right),H=this.right-x,V=A-O,tt=z(i.left)+O,nt=i.right;else if("right"===r)A=z(this.left),tt=i.left,nt=z(i.right)-O,H=A+O,V=this.left+x;else if("x"===t){if("center"===r)A=z((i.top+i.bottom)/2+.5);else if((0,u.i)(r)){const ut=Object.keys(r)[0];A=z(this.chart.scales[ut].getPixelForValue(r[ut]))}J=i.top,Mt=i.bottom,W=A+O,U=W+x}else if("y"===t){if("center"===r)A=z((i.left+i.right)/2);else if((0,u.i)(r)){const ut=Object.keys(r)[0];A=z(this.chart.scales[ut].getPixelForValue(r[ut]))}H=A-O,V=H-x,tt=i.left,nt=i.right}const $t=(0,u.v)(n.ticks.maxTicksLimit,m),at=Math.max(1,Math.ceil(m/$t));for(B=0;Bo.value===i);return n>=0?t.setContext(this.getContext(n)).lineWidth:0}drawGrid(i){const t=this.options.grid,e=this.ctx,n=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(i));let o,r;const l=(c,d,g)=>{!g.width||!g.color||(e.save(),e.lineWidth=g.width,e.strokeStyle=g.color,e.setLineDash(g.borderDash||[]),e.lineDashOffset=g.borderDashOffset,e.beginPath(),e.moveTo(c.x,c.y),e.lineTo(d.x,d.y),e.stroke(),e.restore())};if(t.display)for(o=0,r=n.length;o{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:n,draw:()=>{this.drawBorder()}},{z:t,draw:o=>{this.drawLabels(o)}}]:[{z:t,draw:o=>{this.draw(o)}}]}getMatchingVisibleMetas(i){const t=this.chart.getSortedVisibleDatasetMetas(),e=this.axis+"AxisID",n=[];let o,r;for(o=0,r=t.length;o{const e=t.split("."),n=e.pop(),o=[s].concat(e).join("."),r=i[t].split("."),l=r.pop(),c=r.join(".");u.d.route(o,n,c,l)})}(i,s.defaultRoutes),s.descriptors&&u.d.describe(i,s.descriptors)}(i,r,e),this.override&&u.d.override(i.id,i.overrides)),r}get(i){return this.items[i]}unregister(i){const t=this.items,e=i.id,n=this.scope;e in t&&delete t[e],n&&e in u.d[n]&&(delete u.d[n][e],this.override&&delete u.a3[e])}}class Os{constructor(){this.controllers=new ci(It,"datasets",!0),this.elements=new ci(wt,"elements"),this.plugins=new ci(Object,"plugins"),this.scales=new ci(Nt,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...i){this._each("register",i)}remove(...i){this._each("unregister",i)}addControllers(...i){this._each("register",i,this.controllers)}addElements(...i){this._each("register",i,this.elements)}addPlugins(...i){this._each("register",i,this.plugins)}addScales(...i){this._each("register",i,this.scales)}getController(i){return this._get(i,this.controllers,"controller")}getElement(i){return this._get(i,this.elements,"element")}getPlugin(i){return this._get(i,this.plugins,"plugin")}getScale(i){return this._get(i,this.scales,"scale")}removeControllers(...i){this._each("unregister",i,this.controllers)}removeElements(...i){this._each("unregister",i,this.elements)}removePlugins(...i){this._each("unregister",i,this.plugins)}removeScales(...i){this._each("unregister",i,this.scales)}_each(i,t,e){[...t].forEach(n=>{const o=e||this._getRegistryForType(n);e||o.isForType(n)||o===this.plugins&&n.id?this._exec(i,o,n):(0,u.F)(n,r=>{const l=e||this._getRegistryForType(r);this._exec(i,l,r)})})}_exec(i,t,e){const n=(0,u.a5)(i);(0,u.Q)(e["before"+n],[],e),t[i](e),(0,u.Q)(e["after"+n],[],e)}_getRegistryForType(i){for(let t=0;to.filter(l=>!r.some(c=>l.plugin.id===c.plugin.id));this._notify(n(t,e),i,"stop"),this._notify(n(e,t),i,"start")}}function Vi(s,i){return i||!1!==s?!0===s?{}:s:null}function _n(s,{plugin:i,local:t},e,n){const o=s.pluginScopeKeys(i),r=s.getOptionScopes(e,o);return t&&i.defaults&&r.push(i.defaults),s.createResolver(r,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function Re(s,i){return((i.datasets||{})[s]||{}).indexAxis||i.indexAxis||(u.d.datasets[s]||{}).indexAxis||"x"}function xn(s){if("x"===s||"y"===s||"r"===s)return s}function yn(s){return"top"===s||"bottom"===s?"x":"left"===s||"right"===s?"y":void 0}function di(s,...i){if(xn(s))return s;for(const t of i){const e=t.axis||yn(t.position)||s.length>1&&xn(s[0].toLowerCase());if(e)return e}throw new Error(`Cannot determine type of '${s}' axis. Please provide 'axis' or 'position' option.`)}function ui(s,i,t){if(t[i+"AxisID"]===s)return{axis:i}}function vn(s){const i=s.options||(s.options={});i.plugins=(0,u.v)(i.plugins,{}),i.scales=function Ts(s,i){const t=u.a3[s.type]||{scales:{}},e=i.scales||{},n=Re(s.type,i),o=Object.create(null);return Object.keys(e).forEach(r=>{const l=e[r];if(!(0,u.i)(l))return console.error(`Invalid scale configuration for scale: ${r}`);if(l._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${r}`);const c=di(r,l,function As(s,i){if(i.data&&i.data.datasets){const t=i.data.datasets.filter(e=>e.xAxisID===s||e.yAxisID===s);if(t.length)return ui(s,"x",t[0])||ui(s,"y",t[0])}return{}}(r,s),u.d.scales[l.type]),d=function Wi(s,i){return s===i?"_index_":"_value_"}(c,n),g=t.scales||{};o[r]=(0,u.ab)(Object.create(null),[{axis:c},l,g[c],g[d]])}),s.data.datasets.forEach(r=>{const l=r.type||s.type,c=r.indexAxis||Re(l,i),g=(u.a3[l]||{}).scales||{};Object.keys(g).forEach(m=>{const x=function Me(s,i){let t=s;return"_index_"===s?t=i:"_value_"===s&&(t="x"===i?"y":"x"),t}(m,c),v=r[x+"AxisID"]||x;o[v]=o[v]||Object.create(null),(0,u.ab)(o[v],[{axis:x},e[v],g[m]])})}),Object.keys(o).forEach(r=>{const l=o[r];(0,u.ab)(l,[u.d.scales[l.type],u.d.scale])}),o}(s,i)}function Mn(s){return(s=s||{}).datasets=s.datasets||[],s.labels=s.labels||[],s}const Ie=new Map,wn=new Set;function fi(s,i){let t=Ie.get(s);return t||(t=i(),Ie.set(s,t),wn.add(t)),t}const Fe=(s,i,t)=>{const e=(0,u.f)(i,t);void 0!==e&&s.add(e)};class po{constructor(i){this._config=function Sn(s){return(s=s||{}).data=Mn(s.data),vn(s),s}(i),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(i){this._config.type=i}get data(){return this._config.data}set data(i){this._config.data=Mn(i)}get options(){return this._config.options}set options(i){this._config.options=i}get plugins(){return this._config.plugins}update(){const i=this._config;this.clearCache(),vn(i)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(i){return fi(i,()=>[[`datasets.${i}`,""]])}datasetAnimationScopeKeys(i,t){return fi(`${i}.transition.${t}`,()=>[[`datasets.${i}.transitions.${t}`,`transitions.${t}`],[`datasets.${i}`,""]])}datasetElementScopeKeys(i,t){return fi(`${i}-${t}`,()=>[[`datasets.${i}.elements.${t}`,`datasets.${i}`,`elements.${t}`,""]])}pluginScopeKeys(i){const t=i.id;return fi(`${this.type}-plugin-${t}`,()=>[[`plugins.${t}`,...i.additionalOptionScopes||[]]])}_cachedScopes(i,t){const e=this._scopeCache;let n=e.get(i);return(!n||t)&&(n=new Map,e.set(i,n)),n}getOptionScopes(i,t,e){const{options:n,type:o}=this,r=this._cachedScopes(i,e),l=r.get(t);if(l)return l;const c=new Set;t.forEach(g=>{i&&(c.add(i),g.forEach(m=>Fe(c,i,m))),g.forEach(m=>Fe(c,n,m)),g.forEach(m=>Fe(c,u.a3[o]||{},m)),g.forEach(m=>Fe(c,u.d,m)),g.forEach(m=>Fe(c,u.a6,m))});const d=Array.from(c);return 0===d.length&&d.push(Object.create(null)),wn.has(t)&&r.set(t,d),d}chartOptionScopes(){const{options:i,type:t}=this;return[i,u.a3[t]||{},u.d.datasets[t]||{},{type:t},u.d,u.a6]}resolveNamedOptions(i,t,e,n=[""]){const o={$shared:!0},{resolver:r,subPrefixes:l}=kn(this._resolverCache,i,n);let c=r;if(function zs(s,i){const{isScriptable:t,isIndexable:e}=(0,u.aa)(s);for(const n of i){const o=t(n),r=e(n),l=(r||o)&&s[n];if(o&&((0,u.a7)(l)||Pn(l))||r&&(0,u.b)(l))return!0}return!1}(r,t)){o.$shared=!1,e=(0,u.a7)(e)?e():e;const d=this.createResolver(i,e,l);c=(0,u.a8)(r,e,d)}for(const d of t)o[d]=c[d];return o}createResolver(i,t,e=[""],n){const{resolver:o}=kn(this._resolverCache,i,e);return(0,u.i)(t)?(0,u.a8)(o,t,void 0,n):o}}function kn(s,i,t){let e=s.get(i);e||(e=new Map,s.set(i,e));const n=t.join();let o=e.get(n);return o||(o={resolver:(0,u.a9)(i,t),subPrefixes:t.filter(l=>!l.toLowerCase().includes("hover"))},e.set(n,o)),o}const Pn=s=>(0,u.i)(s)&&Object.getOwnPropertyNames(s).reduce((i,t)=>i||(0,u.a7)(s[t]),!1),Cn=["top","bottom","left","right","chartArea"];function On(s,i){return"top"===s||"bottom"===s||-1===Cn.indexOf(s)&&"x"===i}function Dn(s,i){return function(t,e){return t[s]===e[s]?t[i]-e[i]:t[s]-e[s]}}function An(s){const i=s.chart,t=i.options.animation;i.notifyPlugins("afterRender"),(0,u.Q)(t&&t.onComplete,[s],i)}function Be(s){const i=s.chart,t=i.options.animation;(0,u.Q)(t&&t.onProgress,[s],i)}function gi(s){return(0,u.M)()&&"string"==typeof s?s=document.getElementById(s):s&&s.length&&(s=s[0]),s&&s.canvas&&(s=s.canvas),s}const He={},Yi=s=>{const i=gi(s);return Object.values(He).filter(t=>t.canvas===i).pop()};function Tn(s,i,t){const e=Object.keys(s);for(const n of e){const o=+n;if(o>=i){const r=s[n];delete s[n],(t>0||o>i)&&(s[o+t]=r)}}}let zn=(()=>{class s{static register(...t){Vt.add(...t),je()}static unregister(...t){Vt.remove(...t),je()}constructor(t,e){const n=this.config=new po(e),o=gi(t),r=Yi(o);if(r)throw new Error("Canvas is already in use. Chart with ID '"+r.id+"' must be destroyed before the canvas with ID '"+r.canvas.id+"' can be reused.");const l=n.createResolver(n.chartOptionScopes(),this.getContext());this.platform=new(n.platform||function vs(s){return!(0,u.M)()||typeof OffscreenCanvas<"u"&&s instanceof OffscreenCanvas?Ii:ys}(o)),this.platform.updateConfig(n);const c=this.platform.acquireContext(o,l.aspectRatio),d=c&&c.canvas,g=d&&d.height,m=d&&d.width;this.id=(0,u.ac)(),this.ctx=c,this.canvas=d,this.width=m,this.height=g,this._options=l,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new mn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=(0,u.ad)(x=>this.update(x),l.resizeDelay||0),this._dataChanges=[],He[this.id]=this,c&&d?(Ot.listen(this,"complete",An),Ot.listen(this,"progress",Be),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:n,height:o,_aspectRatio:r}=this;return(0,u.k)(t)?e&&r?r:o?n/o:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return Vt}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():(0,u.ae)(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return(0,u.af)(this.canvas,this.ctx),this}stop(){return Ot.stop(this),this}resize(t,e){Ot.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const n=this.options,l=this.platform.getMaximumSize(this.canvas,t,e,n.maintainAspectRatio&&this.aspectRatio),c=n.devicePixelRatio||this.platform.getDevicePixelRatio(),d=this.width?"resize":"attach";this.width=l.width,this.height=l.height,this._aspectRatio=this.aspectRatio,(0,u.ae)(this,c,!0)&&(this.notifyPlugins("resize",{size:l}),(0,u.Q)(n.onResize,[this,l],this),this.attached&&this._doResize(d)&&this.render())}ensureScalesHaveIDs(){(0,u.F)(this.options.scales||{},(n,o)=>{n.id=o})}buildOrUpdateScales(){const t=this.options,e=t.scales,n=this.scales,o=Object.keys(n).reduce((l,c)=>(l[c]=!1,l),{});let r=[];e&&(r=r.concat(Object.keys(e).map(l=>{const c=e[l],d=di(l,c),g="r"===d,m="x"===d;return{options:c,dposition:g?"chartArea":m?"bottom":"left",dtype:g?"radialLinear":m?"category":"linear"}}))),(0,u.F)(r,l=>{const c=l.options,d=c.id,g=di(d,c),m=(0,u.v)(c.type,l.dtype);(void 0===c.position||On(c.position,g)!==On(l.dposition))&&(c.position=l.dposition),o[d]=!0;let x=null;d in n&&n[d].type===m?x=n[d]:(x=new(Vt.getScale(m))({id:d,type:m,ctx:this.ctx,chart:this}),n[x.id]=x),x.init(c,t)}),(0,u.F)(o,(l,c)=>{l||delete n[c]}),(0,u.F)(n,l=>{gt.configure(this,l,l.options),gt.addBox(this,l)})}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,n=t.length;if(t.sort((o,r)=>o.index-r.index),n>e){for(let o=e;oe.length&&delete this._stacks,t.forEach((n,o)=>{0===e.filter(r=>r===n._dataset).length&&this._destroyDatasetMeta(o)})}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let n,o;for(this._removeUnreferencedMetasets(),n=0,o=e.length;n{this.getDatasetMeta(e).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const n=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),o=this._animationsDisabled=!n.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const r=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let l=0;for(let g=0,m=this.data.datasets.length;g{g.reset()}),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Dn("z","_idx"));const{_active:c,_lastEvent:d}=this;d?this._eventHandler(d,!0):c.length&&this._updateHoverStyles(c,c,!0),this.render()}_updateScales(){(0,u.F)(this.scales,t=>{gt.removeBox(this,t)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),n=new Set(t.events);(!(0,u.ag)(e,n)||!!this._responsiveListeners!==t.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:n,start:o,count:r}of e)Tn(t,o,"_removeElements"===n?-r:r)}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,n=r=>new Set(t.filter(l=>l[0]===r).map((l,c)=>c+","+l.splice(1).join(","))),o=n(0);for(let r=1;rr.split(",")).map(r=>({method:r[1],start:+r[2],count:+r[3]}))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;gt.update(this,this.width,this.height,t);const e=this.chartArea,n=e.width<=0||e.height<=0;this._layers=[],(0,u.F)(this.boxes,o=>{n&&"chartArea"===o.position||(o.configure&&o.configure(),this._layers.push(...o._layers()))},this),this._layers.forEach((o,r)=>{o._idx=r}),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let e=0,n=this.data.datasets.length;e=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,n=t._clip,o=!n.disabled,r=function Rs(s){const{xScale:i,yScale:t}=s;if(i&&t)return{left:i.left,right:i.right,top:t.top,bottom:t.bottom}}(t)||this.chartArea,l={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",l)&&(o&&(0,u.Y)(e,{left:!1===n.left?0:r.left-n.left,right:!1===n.right?this.width:r.right+n.right,top:!1===n.top?0:r.top-n.top,bottom:!1===n.bottom?this.height:r.bottom+n.bottom}),t.controller.draw(),o&&(0,u.$)(e),l.cancelable=!1,this.notifyPlugins("afterDatasetDraw",l))}isPointInArea(t){return(0,u.C)(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,n,o){const r=fo.modes[e];return"function"==typeof r?r(this,t,n,o):[]}getDatasetMeta(t){const e=this.data.datasets[t],n=this._metasets;let o=n.filter(r=>r&&r._dataset===e).pop();return o||(o={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},n.push(o)),o}getContext(){return this.$context||(this.$context=(0,u.j)(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const n=this.getDatasetMeta(t);return"boolean"==typeof n.hidden?!n.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,n){const o=n?"show":"hide",r=this.getDatasetMeta(t),l=r.controller._resolveAnimations(void 0,o);(0,u.h)(e)?(r.data[e].hidden=!n,this.update()):(this.setDatasetVisibility(t,n),l.update(r,{visible:n}),this.update(c=>c.datasetIndex===t?o:void 0))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),Ot.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,r,l),t[r]=l},o=(r,l,c)=>{r.offsetX=l,r.offsetY=c,this._eventHandler(r)};(0,u.F)(this.options.events,r=>n(r,o))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,n=(d,g)=>{e.addEventListener(this,d,g),t[d]=g},o=(d,g)=>{t[d]&&(e.removeEventListener(this,d,g),delete t[d])},r=(d,g)=>{this.canvas&&this.resize(d,g)};let l;const c=()=>{o("attach",c),this.attached=!0,this.resize(),n("resize",r),n("detach",l)};l=()=>{this.attached=!1,o("resize",r),this._stop(),this._resize(0,0),n("attach",c)},e.isAttached(this.canvas)?c():l()}unbindEvents(){(0,u.F)(this._listeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._listeners={},(0,u.F)(this._responsiveListeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._responsiveListeners=void 0}updateHoverStyle(t,e,n){const o=n?"set":"remove";let r,l,c,d;for("dataset"===e&&(r=this.getDatasetMeta(t[0].datasetIndex),r.controller["_"+o+"DatasetHoverStyle"]()),c=0,d=t.length;c{const c=this.getDatasetMeta(r);if(!c)throw new Error("No dataset found at index "+r);return{datasetIndex:r,element:c.data[l],index:l}});!(0,u.ah)(n,e)&&(this._active=n,this._lastEvent=null,this._updateHoverStyles(n,e))}notifyPlugins(t,e,n){return this._plugins.notify(this,t,e,n)}isPluginEnabled(t){return 1===this._plugins._cache.filter(e=>e.plugin.id===t).length}_updateHoverStyles(t,e,n){const o=this.options.hover,r=(d,g)=>d.filter(m=>!g.some(x=>m.datasetIndex===x.datasetIndex&&m.index===x.index)),l=r(e,t),c=n?t:r(t,e);l.length&&this.updateHoverStyle(l,o.mode,!1),c.length&&o.mode&&this.updateHoverStyle(c,o.mode,!0)}_eventHandler(t,e){const n={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},o=l=>(l.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",n,o))return;const r=this._handleEvent(t,e,n.inChartArea);return n.cancelable=!1,this.notifyPlugins("afterEvent",n,o),(r||n.changed)&&this.render(),this}_handleEvent(t,e,n){const{_active:o=[],options:r}=this,c=this._getActiveElements(t,o,n,e),d=(0,u.ai)(t),g=function Es(s,i,t,e){return t&&"mouseout"!==s.type?e?i:s:null}(t,this._lastEvent,n,d);n&&(this._lastEvent=null,(0,u.Q)(r.onHover,[t,c,this],this),d&&(0,u.Q)(r.onClick,[t,c,this],this));const m=!(0,u.ah)(c,o);return(m||e)&&(this._active=c,this._updateHoverStyles(c,o,e)),this._lastEvent=g,m}_getActiveElements(t,e,n,o){if("mouseout"===t.type)return[];if(!n)return e;const r=this.options.hover;return this.getElementsAtEventForMode(t,r.mode,r,o)}}return(0,R.Z)(s,"defaults",u.d),(0,R.Z)(s,"instances",He),(0,R.Z)(s,"overrides",u.a3),(0,R.Z)(s,"registry",Vt),(0,R.Z)(s,"version","4.3.3"),(0,R.Z)(s,"getChart",Yi),s})();function je(){return(0,u.F)(zn.instances,s=>s._plugins.invalidate())}function Se(s,i,t,e){return{x:t+s*Math.cos(i),y:e+s*Math.sin(i)}}function le(s,i,t,e,n,o){const{x:r,y:l,startAngle:c,pixelMargin:d,innerRadius:g}=i,m=Math.max(i.outerRadius+e+t-d,0),x=g>0?g+e+t+d:0;let v=0;const S=n-c;if(e){const pt=((g>0?g-e:0)+(m>0?m-e:0))/2;v=(S-(0!==pt?S*pt/(pt+e):S))/2}const O=(S-Math.max(.001,S*m-t/u.P)/m)/2,z=c+O+v,A=n-O-v,{outerStart:B,outerEnd:j,innerStart:L,innerEnd:H}=function Ln(s,i,t,e){const n=function ae(s){return(0,u.ak)(s,["outerStart","outerEnd","innerStart","innerEnd"])}(s.options.borderRadius),o=(t-i)/2,r=Math.min(o,e*i/2),l=c=>{const d=(t-Math.min(o,c))*e/2;return(0,u.S)(c,0,Math.min(o,d))};return{outerStart:l(n.outerStart),outerEnd:l(n.outerEnd),innerStart:(0,u.S)(n.innerStart,0,r),innerEnd:(0,u.S)(n.innerEnd,0,r)}}(i,x,m,A-z),W=m-B,V=m-j,U=z+B/W,tt=A-j/V,J=x+L,nt=x+H,Mt=z+L/J,$t=A-H/nt;if(s.beginPath(),o){const at=(U+tt)/2;if(s.arc(r,l,m,U,at),s.arc(r,l,m,at,tt),j>0){const Ct=Se(V,tt,r,l);s.arc(Ct.x,Ct.y,j,tt,A+u.H)}const ut=Se(nt,A,r,l);if(s.lineTo(ut.x,ut.y),H>0){const Ct=Se(nt,$t,r,l);s.arc(Ct.x,Ct.y,H,A+u.H,$t+Math.PI)}const pt=(A-H/x+(z+L/x))/2;if(s.arc(r,l,x,A-H/x,pt,!0),s.arc(r,l,x,pt,z+L/x,!0),L>0){const Ct=Se(J,Mt,r,l);s.arc(Ct.x,Ct.y,L,Mt+Math.PI,z-u.H)}const he=Se(W,z,r,l);if(s.lineTo(he.x,he.y),B>0){const Ct=Se(W,U,r,l);s.arc(Ct.x,Ct.y,B,z-u.H,U)}}else{s.moveTo(r,l);const at=Math.cos(U)*m+r,ut=Math.sin(U)*m+l;s.lineTo(at,ut);const pt=Math.cos(tt)*m+r,he=Math.sin(tt)*m+l;s.lineTo(pt,he)}s.closePath()}class we extends wt{constructor(i){super(),(0,R.Z)(this,"circumference",void 0),(0,R.Z)(this,"endAngle",void 0),(0,R.Z)(this,"fullCircles",void 0),(0,R.Z)(this,"innerRadius",void 0),(0,R.Z)(this,"outerRadius",void 0),(0,R.Z)(this,"pixelMargin",void 0),(0,R.Z)(this,"startAngle",void 0),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,i&&Object.assign(this,i)}inRange(i,t,e){const n=this.getProps(["x","y"],e),{angle:o,distance:r}=(0,u.D)(n,{x:i,y:t}),{startAngle:l,endAngle:c,innerRadius:d,outerRadius:g,circumference:m}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],e),x=(this.options.spacing+this.options.borderWidth)/2,S=(0,u.v)(m,c-l)>=u.T||(0,u.p)(o,l,c),P=(0,u.aj)(r,d+x,g+x);return S&&P}getCenterPoint(i){const{x:t,y:e,startAngle:n,endAngle:o,innerRadius:r,outerRadius:l}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],i),{offset:c,spacing:d}=this.options,g=(n+o)/2,m=(r+l+d+c)/2;return{x:t+Math.cos(g)*m,y:e+Math.sin(g)*m}}tooltipPosition(i){return this.getCenterPoint(i)}draw(i){const{options:t,circumference:e}=this,n=(t.offset||0)/4,o=(t.spacing||0)/2,r=t.circular;if(this.pixelMargin="inner"===t.borderAlign?.33:0,this.fullCircles=e>u.T?Math.floor(e/u.T):0,0===e||this.innerRadius<0||this.outerRadius<0)return;i.save();const l=(this.startAngle+this.endAngle)/2;i.translate(Math.cos(l)*n,Math.sin(l)*n);const d=n*(1-Math.sin(Math.min(u.P,e||0)));i.fillStyle=t.backgroundColor,i.strokeStyle=t.borderColor,function En(s,i,t,e,n){const{fullCircles:o,startAngle:r,circumference:l}=i;let c=i.endAngle;if(o){le(s,i,t,e,c,n);for(let d=0;dn?(d=n/c,s.arc(o,r,c,t+d,e-d,!0)):s.arc(o,r,n,t+u.H,e-u.H),s.closePath(),s.clip()}(s,i,S),o||(le(s,i,t,e,S,n),s.stroke())}(i,this,d,o,r),i.restore()}}function Rn(s,i,t=i){s.lineCap=(0,u.v)(t.borderCapStyle,i.borderCapStyle),s.setLineDash((0,u.v)(t.borderDash,i.borderDash)),s.lineDashOffset=(0,u.v)(t.borderDashOffset,i.borderDashOffset),s.lineJoin=(0,u.v)(t.borderJoinStyle,i.borderJoinStyle),s.lineWidth=(0,u.v)(t.borderWidth,i.borderWidth),s.strokeStyle=(0,u.v)(t.borderColor,i.borderColor)}function In(s,i,t){s.lineTo(t.x,t.y)}function Zi(s,i,t={}){const e=s.length,{start:n=0,end:o=e-1}=t,{start:r,end:l}=i,c=Math.max(n,r),d=Math.min(o,l);return{count:e,start:c,loop:i.loop,ilen:dl&&o>l)?e+d-c:d-c}}function pi(s,i,t,e){const{points:n,options:o}=i,{count:r,start:l,loop:c,ilen:d}=Zi(n,t,e),g=function Is(s){return s.stepped?u.ar:s.tension||"monotone"===s.cubicInterpolationMode?u.as:In}(o);let v,S,P,{move:m=!0,reverse:x}=e||{};for(v=0;v<=d;++v)S=n[(l+(x?d-v:v))%r],!S.skip&&(m?(s.moveTo(S.x,S.y),m=!1):g(s,P,S,x,o.stepped),P=S);return c&&(S=n[(l+(x?d:0))%r],g(s,P,S,x,o.stepped)),!!c}function Fn(s,i,t,e){const n=i.points,{count:o,start:r,ilen:l}=Zi(n,t,e),{move:c=!0,reverse:d}=e||{};let x,v,S,P,O,z,g=0,m=0;const A=j=>(r+(d?l-j:j))%o,B=()=>{P!==O&&(s.lineTo(g,O),s.lineTo(g,P),s.lineTo(g,z))};for(c&&(v=n[A(0)],s.moveTo(v.x,v.y)),x=0;x<=l;++x){if(v=n[A(x)],v.skip)continue;const j=v.x,L=v.y,H=0|j;H===S?(LO&&(O=L),g=(m*g+j)/++m):(B(),s.lineTo(j,L),S=H,m=0,P=O=L),z=L}B()}function mi(s){const i=s.options;return s._decimated||s._loop||i.tension||"monotone"===i.cubicInterpolationMode||i.stepped||i.borderDash&&i.borderDash.length?pi:Fn}(0,R.Z)(we,"id","arc"),(0,R.Z)(we,"defaults",{borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0}),(0,R.Z)(we,"defaultRoutes",{backgroundColor:"backgroundColor"}),(0,R.Z)(we,"descriptors",{_scriptable:!0,_indexable:s=>"borderDash"!==s});const Bs="function"==typeof Path2D;let bi=(()=>{class s extends wt{constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const n=this.options;!n.tension&&"monotone"!==n.cubicInterpolationMode||n.stepped||this._pointsUpdated||((0,u.al)(this._points,n,t,n.spanGaps?this._loop:this._fullLoop,e),this._pointsUpdated=!0)}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=(0,u.am)(this,this.options.segment))}first(){const t=this.segments;return t.length&&this.points[t[0].start]}last(){const t=this.segments,n=t.length;return n&&this.points[t[n-1].end]}interpolate(t,e){const n=this.options,o=t[e],r=this.points,l=(0,u.an)(this,{property:e,start:o,end:o});if(!l.length)return;const c=[],d=function Fs(s){return s.stepped?u.ao:s.tension||"monotone"===s.cubicInterpolationMode?u.ap:u.aq}(n);let g,m;for(g=0,m=l.length;g"borderDash"!==i&&"fill"!==i}),s})();function Kt(s,i,t,e){const n=s.options,{[t]:o}=s.getProps([t],e);return Math.abs(i-o){class s extends wt{constructor(t){super(),(0,R.Z)(this,"parsed",void 0),(0,R.Z)(this,"skip",void 0),(0,R.Z)(this,"stop",void 0),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,n){const o=this.options,{x:r,y:l}=this.getProps(["x","y"],n);return Math.pow(t-r,2)+Math.pow(e-l,2)s.replace("rgb(","rgba(").replace(")",", 0.5)"));function qt(s){return Qi[s%Qi.length]}function Nn(s){return jn[s%jn.length]}function Vn(s){let i;for(i in s)if(s[i].borderColor||s[i].backgroundColor)return!0;return!1}var Xs={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(s,i,t){if(!t.enabled)return;const{data:{datasets:e},options:n}=s.config,{elements:o}=n;if(!t.forceOverride&&(Vn(e)||function Zs(s){return s&&(s.borderColor||s.backgroundColor)}(n)||o&&Vn(o)))return;const r=function vi(s){let i=0;return(t,e)=>{const n=s.getDatasetMeta(e).controller;n instanceof zi?i=function $s(s,i){return s.backgroundColor=s.data.map(()=>qt(i++)),i}(t,i):n instanceof _t?i=function Us(s,i){return s.backgroundColor=s.data.map(()=>Nn(i++)),i}(t,i):n&&(i=function Ys(s,i){return s.borderColor=qt(i),s.backgroundColor=Nn(i),++i}(t,i))}}(s);e.forEach(r)}};function Wn(s){if(s._decimated){const i=s._data;delete s._decimated,delete s._data,Object.defineProperty(s,"data",{configurable:!0,enumerable:!0,writable:!0,value:i})}}function Yn(s){s.data.datasets.forEach(i=>{Wn(i)})}var rt={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(s,i,t)=>{if(!t.enabled)return void Yn(s);const e=s.width;s.data.datasets.forEach((n,o)=>{const{_data:r,indexAxis:l}=n,c=s.getDatasetMeta(o),d=r||n.data;if("y"===(0,u.a)([l,s.options.indexAxis])||!c.controller.supportsDecimation)return;const g=s.scales[c.xAxisID];if("linear"!==g.type&&"time"!==g.type||s.options.parsing)return;let S,{start:m,count:x}=function Gs(s,i){const t=i.length;let n,e=0;const{iScale:o}=s,{min:r,max:l,minDefined:c,maxDefined:d}=o.getUserBounds();return c&&(e=(0,u.S)((0,u.B)(i,o.axis,r).lo,0,t-1)),n=d?(0,u.S)((0,u.B)(i,o.axis,l).hi+1,e,t)-e:t-e,{start:e,count:n}}(c,d);if(x<=(t.threshold||4*e))Wn(n);else{switch((0,u.k)(r)&&(n._data=d,delete n.data,Object.defineProperty(n,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(P){this._data=P}})),t.algorithm){case"lttb":S=function Ks(s,i,t,e,n){const o=n.samples||e;if(o>=t)return s.slice(i,i+t);const r=[],l=(t-2)/(o-2);let c=0;const d=i+t-1;let m,x,v,S,P,g=i;for(r[c++]=s[g],m=0;mv&&(v=S,x=s[A],P=A);r[c++]=x,g=P}return r[c++]=s[d],r}(d,m,x,e,t);break;case"min-max":S=function ce(s,i,t,e){let r,l,c,d,g,m,x,v,S,P,n=0,o=0;const O=[],A=s[i].x,j=s[i+t-1].x-A;for(r=i;rP&&(P=d,x=r),n=(o*n+l.x)/++o;else{const H=r-1;if(!(0,u.k)(m)&&!(0,u.k)(x)){const W=Math.min(m,x),V=Math.max(m,x);W!==v&&W!==H&&O.push({...s[W],x:n}),V!==v&&V!==H&&O.push({...s[V],x:n})}r>0&&H!==v&&O.push(s[H]),O.push(l),g=L,o=0,S=P=d,m=x=v=r}}return O}(d,m,x,e);break;default:throw new Error(`Unsupported decimation algorithm '${t.algorithm}'`)}n._decimated=S}})},destroy(s){Yn(s)}};function qi(s,i,t,e){if(e)return;let n=i[s],o=t[s];return"angle"===s&&(n=(0,u.ay)(n),o=(0,u.ay)(o)),{property:s,start:n,end:o}}function Mi(s,i,t){for(;i>s;i--){const e=t[i];if(!isNaN(e.x)&&!isNaN(e.y))break}return i}function Si(s,i,t,e){return s&&i?e(s[t],i[t]):s?s[t]:i?i[t]:0}function ke(s,i){let t=[],e=!1;return(0,u.b)(s)?(e=!0,t=s):t=function qs(s,i){const{x:t=null,y:e=null}=s||{},n=i.points,o=[];return i.segments.forEach(({start:r,end:l})=>{l=Mi(r,l,n);const c=n[r],d=n[l];null!==e?(o.push({x:c.x,y:e}),o.push({x:d.x,y:e})):null!==t&&(o.push({x:t,y:c.y}),o.push({x:t,y:d.y}))}),o}(s,i),t.length?new bi({points:t,options:{tension:0},_loop:e,_fullLoop:e}):null}function Ji(s){return s&&!1!==s.fill}function tn(s,i,t){let n=s[i].fill;const o=[i];let r;if(!t)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!(0,u.g)(n))return n;if(r=s[n],!r)return!1;if(r.visible)return n;o.push(n),n=r.fill}return!1}function Js(s,i,t){const e=function Xn(s){const i=s.options,t=i.fill;let e=(0,u.v)(t&&t.target,t);return void 0===e&&(e=!!i.backgroundColor),!1!==e&&null!==e&&(!0===e?"origin":e)}(s);if((0,u.i)(e))return!isNaN(e.value)&&e;let n=parseFloat(e);return(0,u.g)(n)&&Math.floor(n)===n?function $n(s,i,t,e){return("-"===s||"+"===s)&&(t=i+t),!(t===i||t<0||t>=e)&&t}(e[0],i,n,t):["origin","start","end","stack","shape"].indexOf(e)>=0&&e}function a(s,i,t){const e=[];for(let n=0;n=0;--r){const l=n[r].$filler;l&&(l.line.updateControlPoints(o,l.axis),e&&l.fill&&C(s.ctx,l,o))}},beforeDatasetsDraw(s,i,t){if("beforeDatasetsDraw"!==t.drawTime)return;const e=s.getSortedVisibleDatasetMetas();for(let n=e.length-1;n>=0;--n){const o=e[n].$filler;Ji(o)&&C(s.ctx,o,s.chartArea)}},beforeDatasetDraw(s,i,t){const e=i.meta.$filler;!Ji(e)||"beforeDatasetDraw"!==t.drawTime||C(s.ctx,e,s.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const K=(s,i)=>{let{boxHeight:t=i,boxWidth:e=i}=s;return s.usePointStyle&&(t=Math.min(t,i),e=s.pointStyleWidth||Math.min(e,i)),{boxWidth:e,boxHeight:t,itemHeight:Math.max(i,t)}};class bt extends wt{constructor(i){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=i.chart,this.options=i.options,this.ctx=i.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(i,t,e){this.maxWidth=i,this.maxHeight=t,this._margins=e,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const i=this.options.labels||{};let t=(0,u.Q)(i.generateLabels,[this.chart],this)||[];i.filter&&(t=t.filter(e=>i.filter(e,this.chart.data))),i.sort&&(t=t.sort((e,n)=>i.sort(e,n,this.chart.data))),this.options.reverse&&t.reverse(),this.legendItems=t}fit(){const{options:i,ctx:t}=this;if(!i.display)return void(this.width=this.height=0);const e=i.labels,n=(0,u.a0)(e.font),o=n.size,r=this._computeTitleHeight(),{boxWidth:l,itemHeight:c}=K(e,o);let d,g;t.font=n.string,this.isHorizontal()?(d=this.maxWidth,g=this._fitRows(r,o,l,c)+10):(g=this.maxHeight,d=this._fitCols(r,n,l,c)+10),this.width=Math.min(d,i.maxWidth||this.maxWidth),this.height=Math.min(g,i.maxHeight||this.maxHeight)}_fitRows(i,t,e,n){const{ctx:o,maxWidth:r,options:{labels:{padding:l}}}=this,c=this.legendHitBoxes=[],d=this.lineWidths=[0],g=n+l;let m=i;o.textAlign="left",o.textBaseline="middle";let x=-1,v=-g;return this.legendItems.forEach((S,P)=>{const O=e+t/2+o.measureText(S.text).width;(0===P||d[d.length-1]+O+2*l>r)&&(m+=g,d[d.length-(P>0?0:1)]=0,v+=g,x++),c[P]={left:0,top:v,row:x,width:O,height:n},d[d.length-1]+=O+l}),m}_fitCols(i,t,e,n){const{ctx:o,maxHeight:r,options:{labels:{padding:l}}}=this,c=this.legendHitBoxes=[],d=this.columnSizes=[],g=r-i;let m=l,x=0,v=0,S=0,P=0;return this.legendItems.forEach((O,z)=>{const{itemWidth:A,itemHeight:B}=function Pe(s,i,t,e,n){const o=function Ne(s,i,t,e){let n=s.text;return n&&"string"!=typeof n&&(n=n.reduce((o,r)=>o.length>r.length?o:r)),i+t.size/2+e.measureText(n).width}(e,s,i,t),r=function to(s,i,t){let e=s;return"string"!=typeof i.text&&(e=Gn(i,t)),e}(n,e,i.lineHeight);return{itemWidth:o,itemHeight:r}}(e,t,o,O,n);z>0&&v+B+2*l>g&&(m+=x+l,d.push({width:x,height:v}),S+=x+l,P++,x=v=0),c[z]={left:S,top:v,col:P,width:A,height:B},x=Math.max(x,A),v+=B+l}),m+=x,d.push({width:x,height:v}),m}adjustHitBoxes(){if(!this.options.display)return;const i=this._computeTitleHeight(),{legendHitBoxes:t,options:{align:e,labels:{padding:n},rtl:o}}=this,r=(0,u.az)(o,this.left,this.width);if(this.isHorizontal()){let l=0,c=(0,u.a2)(e,this.left+n,this.right-this.lineWidths[l]);for(const d of t)l!==d.row&&(l=d.row,c=(0,u.a2)(e,this.left+n,this.right-this.lineWidths[l])),d.top+=this.top+i+n,d.left=r.leftForLtr(r.x(c),d.width),c+=d.width+n}else{let l=0,c=(0,u.a2)(e,this.top+i+n,this.bottom-this.columnSizes[l].height);for(const d of t)d.col!==l&&(l=d.col,c=(0,u.a2)(e,this.top+i+n,this.bottom-this.columnSizes[l].height)),d.top=c,d.left+=this.left+n,d.left=r.leftForLtr(r.x(d.left),d.width),c+=d.height+n}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const i=this.ctx;(0,u.Y)(i,this),this._draw(),(0,u.$)(i)}}_draw(){const{options:i,columnSizes:t,lineWidths:e,ctx:n}=this,{align:o,labels:r}=i,l=u.d.color,c=(0,u.az)(i.rtl,this.left,this.width),d=(0,u.a0)(r.font),{padding:g}=r,m=d.size,x=m/2;let v;this.drawTitle(),n.textAlign=c.textAlign("left"),n.textBaseline="middle",n.lineWidth=.5,n.font=d.string;const{boxWidth:S,boxHeight:P,itemHeight:O}=K(r,m),B=this.isHorizontal(),j=this._computeTitleHeight();v=B?{x:(0,u.a2)(o,this.left+g,this.right-e[0]),y:this.top+g+j,line:0}:{x:this.left+g,y:(0,u.a2)(o,this.top+j+g,this.bottom-t[0].height),line:0},(0,u.aA)(this.ctx,i.textDirection);const L=O+g;this.legendItems.forEach((H,W)=>{n.strokeStyle=H.fontColor,n.fillStyle=H.fontColor;const V=n.measureText(H.text).width,U=c.textAlign(H.textAlign||(H.textAlign=r.textAlign)),tt=S+x+V;let J=v.x,nt=v.y;c.setWidth(this.width),B?W>0&&J+tt+g>this.right&&(nt=v.y+=L,v.line++,J=v.x=(0,u.a2)(o,this.left+g,this.right-e[v.line])):W>0&&nt+L>this.bottom&&(J=v.x=J+t[v.line].width+g,v.line++,nt=v.y=(0,u.a2)(o,this.top+j+g,this.bottom-t[v.line].height)),function(H,W,V){if(isNaN(S)||S<=0||isNaN(P)||P<0)return;n.save();const U=(0,u.v)(V.lineWidth,1);if(n.fillStyle=(0,u.v)(V.fillStyle,l),n.lineCap=(0,u.v)(V.lineCap,"butt"),n.lineDashOffset=(0,u.v)(V.lineDashOffset,0),n.lineJoin=(0,u.v)(V.lineJoin,"miter"),n.lineWidth=U,n.strokeStyle=(0,u.v)(V.strokeStyle,l),n.setLineDash((0,u.v)(V.lineDash,[])),r.usePointStyle){const tt={radius:P*Math.SQRT2/2,pointStyle:V.pointStyle,rotation:V.rotation,borderWidth:U},J=c.xPlus(H,S/2);(0,u.aD)(n,tt,J,W+x,r.pointStyleWidth&&S)}else{const tt=W+Math.max((m-P)/2,0),J=c.leftForLtr(H,S),nt=(0,u.aw)(V.borderRadius);n.beginPath(),Object.values(nt).some(Mt=>0!==Mt)?(0,u.au)(n,{x:J,y:tt,w:S,h:P,radius:nt}):n.rect(J,tt,S,P),n.fill(),0!==U&&n.stroke()}n.restore()}(c.x(J),nt,H),J=(0,u.aB)(U,J+S+x,B?J+tt:this.right,i.rtl),function(H,W,V){(0,u.Z)(n,V.text,H,W+O/2,d,{strikethrough:V.hidden,textAlign:c.textAlign(V.textAlign)})}(c.x(J),nt,H),B?v.x+=tt+g:v.y+="string"!=typeof H.text?Gn(H,d.lineHeight)+g:L}),(0,u.aC)(this.ctx,i.textDirection)}drawTitle(){const i=this.options,t=i.title,e=(0,u.a0)(t.font),n=(0,u.E)(t.padding);if(!t.display)return;const o=(0,u.az)(i.rtl,this.left,this.width),r=this.ctx,l=t.position,d=n.top+e.size/2;let g,m=this.left,x=this.width;if(this.isHorizontal())x=Math.max(...this.lineWidths),g=this.top+d,m=(0,u.a2)(i.align,m,this.right-x);else{const S=this.columnSizes.reduce((P,O)=>Math.max(P,O.height),0);g=d+(0,u.a2)(i.align,this.top,this.bottom-S-i.labels.padding-this._computeTitleHeight())}const v=(0,u.a2)(l,m,m+x);r.textAlign=o.textAlign((0,u.a1)(l)),r.textBaseline="middle",r.strokeStyle=t.color,r.fillStyle=t.color,r.font=e.string,(0,u.Z)(r,t.text,v,g,e)}_computeTitleHeight(){const i=this.options.title,t=(0,u.a0)(i.font),e=(0,u.E)(i.padding);return i.display?t.lineHeight+e.height:0}_getLegendItemAt(i,t){let e,n,o;if((0,u.aj)(i,this.left,this.right)&&(0,u.aj)(t,this.top,this.bottom))for(o=this.legendHitBoxes,e=0;enull!==s&&null!==i&&s.datasetIndex===i.datasetIndex&&s.index===i.index)(n,e);n&&!o&&(0,u.Q)(t.onLeave,[i,n,this],this),this._hoveredItem=e,e&&!o&&(0,u.Q)(t.onHover,[i,e,this],this)}else e&&(0,u.Q)(t.onClick,[i,e,this],this)}}function Gn(s,i){return i*(s.text?s.text.length:0)}var Qn={id:"legend",_element:bt,start(s,i,t){const e=s.legend=new bt({ctx:s.ctx,options:t,chart:s});gt.configure(s,e,t),gt.addBox(s,e)},stop(s){gt.removeBox(s,s.legend),delete s.legend},beforeUpdate(s,i,t){const e=s.legend;gt.configure(s,e,t),e.options=t},afterUpdate(s){const i=s.legend;i.buildLabels(),i.adjustHitBoxes()},afterEvent(s,i){i.replay||s.legend.handleEvent(i.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(s,i,t){const e=i.datasetIndex,n=t.chart;n.isDatasetVisible(e)?(n.hide(e),i.hidden=!0):(n.show(e),i.hidden=!1)},onHover:null,onLeave:null,labels:{color:s=>s.chart.options.color,boxWidth:40,padding:10,generateLabels(s){const i=s.data.datasets,{labels:{usePointStyle:t,pointStyle:e,textAlign:n,color:o,useBorderRadius:r,borderRadius:l}}=s.legend.options;return s._getSortedDatasetMetas().map(c=>{const d=c.controller.getStyle(t?0:void 0),g=(0,u.E)(d.borderWidth);return{text:i[c.index].label,fillStyle:d.backgroundColor,fontColor:o,hidden:!c.visible,lineCap:d.borderCapStyle,lineDash:d.borderDash,lineDashOffset:d.borderDashOffset,lineJoin:d.borderJoinStyle,lineWidth:(g.width+g.height)/4,strokeStyle:d.borderColor,pointStyle:e||d.pointStyle,rotation:d.rotation,textAlign:n||d.textAlign,borderRadius:r&&(l||d.borderRadius),datasetIndex:c.index}},this)}},title:{color:s=>s.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:s=>!s.startsWith("on"),labels:{_scriptable:s=>!["generateLabels","filter","sort"].includes(s)}}};class Jt extends wt{constructor(i){super(),this.chart=i.chart,this.options=i.options,this.ctx=i.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(i,t){const e=this.options;if(this.left=0,this.top=0,!e.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=i,this.height=this.bottom=t;const n=(0,u.b)(e.text)?e.text.length:1;this._padding=(0,u.E)(e.padding);const o=n*(0,u.a0)(e.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const i=this.options.position;return"top"===i||"bottom"===i}_drawArgs(i){const{top:t,left:e,bottom:n,right:o,options:r}=this,l=r.align;let d,g,m,c=0;return this.isHorizontal()?(g=(0,u.a2)(l,e,o),m=t+i,d=o-e):("left"===r.position?(g=e+i,m=(0,u.a2)(l,n,t),c=-.5*u.P):(g=o-i,m=(0,u.a2)(l,t,n),c=.5*u.P),d=n-t),{titleX:g,titleY:m,maxWidth:d,rotation:c}}draw(){const i=this.ctx,t=this.options;if(!t.display)return;const e=(0,u.a0)(t.font),o=e.lineHeight/2+this._padding.top,{titleX:r,titleY:l,maxWidth:c,rotation:d}=this._drawArgs(o);(0,u.Z)(i,t.text,0,0,e,{color:t.color,maxWidth:c,rotation:d,textAlign:(0,u.a1)(t.align),textBaseline:"middle",translation:[r,l]})}}var Lo={id:"title",_element:Jt,start(s,i,t){!function qn(s,i){const t=new Jt({ctx:s.ctx,options:i,chart:s});gt.configure(s,t,i),gt.addBox(s,t),s.titleBlock=t}(s,t)},stop(s){gt.removeBox(s,s.titleBlock),delete s.titleBlock},beforeUpdate(s,i,t){const e=s.titleBlock;gt.configure(s,e,t),e.options=t},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Jn=new WeakMap;var mo={id:"subtitle",start(s,i,t){const e=new Jt({ctx:s.ctx,options:t,chart:s});gt.configure(s,e,t),gt.addBox(s,e),Jn.set(s,e)},stop(s){gt.removeBox(s,Jn.get(s)),Jn.delete(s)},beforeUpdate(s,i,t){const e=Jn.get(s);gt.configure(s,e,t),e.options=t},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const nn={average(s){if(!s.length)return!1;let i,t,e=0,n=0,o=0;for(i=0,t=s.length;i-1?s.split("\n"):s}function Eo(s,i){const{element:t,datasetIndex:e,index:n}=i,o=s.getDatasetMeta(e).controller,{label:r,value:l}=o.getLabelAndValue(n);return{chart:s,label:r,parsed:o.getParsed(n),raw:s.data.datasets[e].data[n],formattedValue:l,dataset:o.getDataset(),dataIndex:n,datasetIndex:e,element:t}}function bo(s,i){const t=s.chart.ctx,{body:e,footer:n,title:o}=s,{boxWidth:r,boxHeight:l}=i,c=(0,u.a0)(i.bodyFont),d=(0,u.a0)(i.titleFont),g=(0,u.a0)(i.footerFont),m=o.length,x=n.length,v=e.length,S=(0,u.E)(i.padding);let P=S.height,O=0,z=e.reduce((j,L)=>j+L.before.length+L.lines.length+L.after.length,0);z+=s.beforeBody.length+s.afterBody.length,m&&(P+=m*d.lineHeight+(m-1)*i.titleSpacing+i.titleMarginBottom),z&&(P+=v*(i.displayColors?Math.max(l,c.lineHeight):c.lineHeight)+(z-v)*c.lineHeight+(z-1)*i.bodySpacing),x&&(P+=i.footerMarginTop+x*g.lineHeight+(x-1)*i.footerSpacing);let A=0;const B=function(j){O=Math.max(O,t.measureText(j).width+A)};return t.save(),t.font=d.string,(0,u.F)(s.title,B),t.font=c.string,(0,u.F)(s.beforeBody.concat(s.afterBody),B),A=i.displayColors?r+2+i.boxPadding:0,(0,u.F)(e,j=>{(0,u.F)(j.before,B),(0,u.F)(j.lines,B),(0,u.F)(j.after,B)}),A=0,t.font=g.string,(0,u.F)(s.footer,B),t.restore(),O+=S.width,{width:O,height:P}}function Io(s,i,t,e){const{x:n,width:o}=t,{width:r,chartArea:{left:l,right:c}}=s;let d="center";return"center"===e?d=n<=(l+c)/2?"left":"right":n<=o/2?d="left":n>=r-o/2&&(d="right"),function Ro(s,i,t,e){const{x:n,width:o}=e,r=t.caretSize+t.caretPadding;if("left"===s&&n+o+r>i.width||"right"===s&&n-o-r<0)return!0}(d,s,i,t)&&(d="center"),d}function xo(s,i,t){const e=t.yAlign||i.yAlign||function _o(s,i){const{y:t,height:e}=i;return ts.height-e/2?"bottom":"center"}(s,t);return{xAlign:t.xAlign||i.xAlign||Io(s,i,t,e),yAlign:e}}function yo(s,i,t,e){const{caretSize:n,caretPadding:o,cornerRadius:r}=s,{xAlign:l,yAlign:c}=t,d=n+o,{topLeft:g,topRight:m,bottomLeft:x,bottomRight:v}=(0,u.aw)(r);let S=function Fo(s,i){let{x:t,width:e}=s;return"right"===i?t-=e:"center"===i&&(t-=e/2),t}(i,l);const P=function Bo(s,i,t){let{y:e,height:n}=s;return"top"===i?e+=t:e-="bottom"===i?n+t:n/2,e}(i,c,d);return"center"===c?"left"===l?S+=d:"right"===l&&(S-=d):"left"===l?S-=Math.max(g,x)+n:"right"===l&&(S+=Math.max(m,v)+n),{x:(0,u.S)(S,0,e.width-i.width),y:(0,u.S)(P,0,e.height-i.height)}}function ts(s,i,t){const e=(0,u.E)(t.padding);return"center"===i?s.x+s.width/2:"right"===i?s.x+s.width-e.right:s.x+e.left}function vo(s){return te([],ee(s))}function Mo(s,i){const t=i&&i.dataset&&i.dataset.tooltip&&i.dataset.tooltip.callbacks;return t?s.override(t):s}const jo={beforeTitle:u.aF,title(s){if(s.length>0){const i=s[0],t=i.chart.data.labels,e=t?t.length:0;if(this&&this.options&&"dataset"===this.options.mode)return i.dataset.label||"";if(i.label)return i.label;if(e>0&&i.dataIndex"u"?jo[i].call(t,e):n}let So=(()=>{class s extends wt{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const e=this.chart,n=this.options.setContext(this.getContext()),o=n.enabled&&e.options.animation&&n.animations,r=new mt(this.chart,o);return o._cacheable&&(this._cachedAnimations=Object.freeze(r)),r}getContext(){return this.$context||(this.$context=function Ho(s,i,t){return(0,u.j)(s,{tooltip:i,tooltipItems:t,type:"tooltip"})}(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,e){const{callbacks:n}=e,o=kt(n,"beforeTitle",this,t),r=kt(n,"title",this,t),l=kt(n,"afterTitle",this,t);let c=[];return c=te(c,ee(o)),c=te(c,ee(r)),c=te(c,ee(l)),c}getBeforeBody(t,e){return vo(kt(e.callbacks,"beforeBody",this,t))}getBody(t,e){const{callbacks:n}=e,o=[];return(0,u.F)(t,r=>{const l={before:[],lines:[],after:[]},c=Mo(n,r);te(l.before,ee(kt(c,"beforeLabel",this,r))),te(l.lines,kt(c,"label",this,r)),te(l.after,ee(kt(c,"afterLabel",this,r))),o.push(l)}),o}getAfterBody(t,e){return vo(kt(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:n}=e,o=kt(n,"beforeFooter",this,t),r=kt(n,"footer",this,t),l=kt(n,"afterFooter",this,t);let c=[];return c=te(c,ee(o)),c=te(c,ee(r)),c=te(c,ee(l)),c}_createItems(t){const e=this._active,n=this.chart.data,o=[],r=[],l=[];let d,g,c=[];for(d=0,g=e.length;dt.filter(m,x,v,n))),t.itemSort&&(c=c.sort((m,x)=>t.itemSort(m,x,n))),(0,u.F)(c,m=>{const x=Mo(t.callbacks,m);o.push(kt(x,"labelColor",this,m)),r.push(kt(x,"labelPointStyle",this,m)),l.push(kt(x,"labelTextColor",this,m))}),this.labelColors=o,this.labelPointStyles=r,this.labelTextColors=l,this.dataPoints=c,c}update(t,e){const n=this.options.setContext(this.getContext()),o=this._active;let r,l=[];if(o.length){const c=nn[n.position].call(this,o,this._eventPosition);l=this._createItems(n),this.title=this.getTitle(l,n),this.beforeBody=this.getBeforeBody(l,n),this.body=this.getBody(l,n),this.afterBody=this.getAfterBody(l,n),this.footer=this.getFooter(l,n);const d=this._size=bo(this,n),g=Object.assign({},c,d),m=xo(this.chart,n,g),x=yo(n,g,m,this.chart);this.xAlign=m.xAlign,this.yAlign=m.yAlign,r={opacity:1,x:x.x,y:x.y,width:d.width,height:d.height,caretX:c.x,caretY:c.y}}else 0!==this.opacity&&(r={opacity:0});this._tooltipItems=l,this.$context=void 0,r&&this._resolveAnimations().update(this,r),t&&n.external&&n.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,n,o){const r=this.getCaretPosition(t,n,o);e.lineTo(r.x1,r.y1),e.lineTo(r.x2,r.y2),e.lineTo(r.x3,r.y3)}getCaretPosition(t,e,n){const{xAlign:o,yAlign:r}=this,{caretSize:l,cornerRadius:c}=n,{topLeft:d,topRight:g,bottomLeft:m,bottomRight:x}=(0,u.aw)(c),{x:v,y:S}=t,{width:P,height:O}=e;let z,A,B,j,L,H;return"center"===r?(L=S+O/2,"left"===o?(z=v,A=z-l,j=L+l,H=L-l):(z=v+P,A=z+l,j=L-l,H=L+l),B=z):(A="left"===o?v+Math.max(d,m)+l:"right"===o?v+P-Math.max(g,x)-l:this.caretX,"top"===r?(j=S,L=j-l,z=A-l,B=A+l):(j=S+O,L=j+l,z=A+l,B=A-l),H=j),{x1:z,x2:A,x3:B,y1:j,y2:L,y3:H}}drawTitle(t,e,n){const o=this.title,r=o.length;let l,c,d;if(r){const g=(0,u.az)(n.rtl,this.x,this.width);for(t.x=ts(this,n.titleAlign,n),e.textAlign=g.textAlign(n.titleAlign),e.textBaseline="middle",l=(0,u.a0)(n.titleFont),c=n.titleSpacing,e.fillStyle=n.titleColor,e.font=l.string,d=0;d0!==B)?(t.beginPath(),t.fillStyle=r.multiKeyBackground,(0,u.au)(t,{x:O,y:P,w:g,h:d,radius:A}),t.fill(),t.stroke(),t.fillStyle=l.backgroundColor,t.beginPath(),(0,u.au)(t,{x:z,y:P+1,w:g-2,h:d-2,radius:A}),t.fill()):(t.fillStyle=r.multiKeyBackground,t.fillRect(O,P,g,d),t.strokeRect(O,P,g,d),t.fillStyle=l.backgroundColor,t.fillRect(z,P+1,g-2,d-2))}t.fillStyle=this.labelTextColors[n]}drawBody(t,e,n){const{body:o}=this,{bodySpacing:r,bodyAlign:l,displayColors:c,boxHeight:d,boxWidth:g,boxPadding:m}=n,x=(0,u.a0)(n.bodyFont);let v=x.lineHeight,S=0;const P=(0,u.az)(n.rtl,this.x,this.width),O=function(U){e.fillText(U,P.x(t.x+S),t.y+v/2),t.y+=v+r},z=P.textAlign(l);let A,B,j,L,H,W,V;for(e.textAlign=l,e.textBaseline="middle",e.font=x.string,t.x=ts(this,z,n),e.fillStyle=n.bodyColor,(0,u.F)(this.beforeBody,O),S=c&&"right"!==z?"center"===l?g/2+m:g+2+m:0,L=0,W=o.length;L0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,n=this.$animations,o=n&&n.x,r=n&&n.y;if(o||r){const l=nn[t.position].call(this,this._active,this._eventPosition);if(!l)return;const c=this._size=bo(this,t),d=Object.assign({},l,this._size),g=xo(e,t,d),m=yo(t,d,g,e);(o._to!==m.x||r._to!==m.y)&&(this.xAlign=g.xAlign,this.yAlign=g.yAlign,this.width=c.width,this.height=c.height,this.caretX=l.x,this.caretY=l.y,this._resolveAnimations().update(this,m))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let n=this.opacity;if(!n)return;this._updateAnimationTarget(e);const o={width:this.width,height:this.height},r={x:this.x,y:this.y};n=Math.abs(n)<.001?0:n;const l=(0,u.E)(e.padding);e.enabled&&(this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length)&&(t.save(),t.globalAlpha=n,this.drawBackground(r,t,o,e),(0,u.aA)(t,e.textDirection),r.y+=l.top,this.drawTitle(r,t,e),this.drawBody(r,t,e),this.drawFooter(r,t,e),(0,u.aC)(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const n=this._active,o=t.map(({datasetIndex:c,index:d})=>{const g=this.chart.getDatasetMeta(c);if(!g)throw new Error("Cannot find a dataset at index "+c);return{datasetIndex:c,element:g.data[d],index:d}}),r=!(0,u.ah)(n,o),l=this._positionChanged(o,e);(r||l)&&(this._active=o,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,n=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const o=this.options,r=this._active||[],l=this._getActiveElements(t,r,e,n),c=this._positionChanged(l,t),d=e||!(0,u.ah)(l,r)||c;return d&&(this._active=l,(o.enabled||o.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),d}_getActiveElements(t,e,n,o){const r=this.options;if("mouseout"===t.type)return[];if(!o)return e;const l=this.chart.getElementsAtEventForMode(t,r.mode,r,n);return r.reverse&&l.reverse(),l}_positionChanged(t,e){const{caretX:n,caretY:o,options:r}=this,l=nn[r.position].call(this,t,e);return!1!==l&&(n!==l.x||o!==l.y)}}return(0,R.Z)(s,"positioners",nn),s})();var Vo=Object.freeze({__proto__:null,Colors:Xs,Decimation:rt,Filler:dt,Legend:Qn,SubTitle:mo,Title:Lo,Tooltip:{id:"tooltip",_element:So,positioners:nn,afterInit(s,i,t){t&&(s.tooltip=new So({chart:s,options:t}))},beforeUpdate(s,i,t){s.tooltip&&s.tooltip.initialize(t)},reset(s,i,t){s.tooltip&&s.tooltip.initialize(t)},afterDraw(s){const i=s.tooltip;if(i&&i._willRender()){const t={tooltip:i};if(!1===s.notifyPlugins("beforeTooltipDraw",{...t,cancelable:!0}))return;i.draw(s.ctx),s.notifyPlugins("afterTooltipDraw",t)}},afterEvent(s,i){s.tooltip&&s.tooltip.handleEvent(i.event,i.replay,i.inChartArea)&&(i.changed=!0)},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(s,i)=>i.bodyFont.size,boxWidth:(s,i)=>i.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:jo},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:s=>"filter"!==s&&"itemSort"!==s&&"external"!==s,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]}});function Uo(s){const i=this.getLabels();return s>=0&&s{class s extends Nt{constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const n=this.getLabels();for(const{index:o,label:r}of e)n[o]===r&&n.splice(o,1);this._addedLabels=[]}super.init(t)}parse(t,e){if((0,u.k)(t))return null;const n=this.getLabels();return((s,i)=>null===s?null:(0,u.S)(Math.round(s),0,i))(e=isFinite(e)&&n[e]===t?e:function Yo(s,i,t,e){const n=s.indexOf(i);return-1===n?((s,i,t,e)=>("string"==typeof i?(t=s.push(i)-1,e.unshift({index:t,label:i})):isNaN(i)&&(t=null),t))(s,i,t,e):n!==s.lastIndexOf(i)?t:n}(n,t,(0,u.v)(e,t),this._addedLabels),n.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:n,max:o}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(n=0),e||(o=this.getLabels().length-1)),this.min=n,this.max=o}buildTicks(){const t=this.min,e=this.max,n=this.options.offset,o=[];let r=this.getLabels();r=0===t&&e===r.length-1?r:r.slice(t,e+1),this._valueRange=Math.max(r.length-(n?0:1),1),this._startValue=this.min-(n?.5:0);for(let l=t;l<=e;l++)o.push({value:l});return o}getLabelForValue(t){return Uo.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}}return(0,R.Z)(s,"id","category"),(0,R.Z)(s,"defaults",{ticks:{callback:Uo}}),s})();function ko(s,i,{horizontal:t,minRotation:e}){const n=(0,u.t)(e),o=(t?Math.sin(n):Math.cos(n))||.001;return Math.min(i/o,.75*i*(""+s).length)}class es extends Nt{constructor(i){super(i),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(i,t){return(0,u.k)(i)||("number"==typeof i||i instanceof Number)&&!isFinite(+i)?null:+i}handleTickRangeOptions(){const{beginAtZero:i}=this.options,{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:n,max:o}=this;const r=c=>n=t?n:c,l=c=>o=e?o:c;if(i){const c=(0,u.s)(n),d=(0,u.s)(o);c<0&&d<0?l(0):c>0&&d>0&&r(0)}if(n===o){let c=0===o?1:Math.abs(.05*o);l(o+c),i||r(n-c)}this.min=n,this.max=o}getTickLimit(){const i=this.options.ticks;let n,{maxTicksLimit:t,stepSize:e}=i;return e?(n=Math.ceil(this.max/e)-Math.floor(this.min/e)+1,n>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${e} would result generating up to ${n} ticks. Limiting to 1000.`),n=1e3)):(n=this.computeTickLimit(),t=t||11),t&&(n=Math.min(t,n)),n}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const i=this.options,t=i.ticks;let e=this.getTickLimit();e=Math.max(2,e);const r=function wo(s,i){const t=[],{bounds:n,step:o,min:r,max:l,precision:c,count:d,maxTicks:g,maxDigits:m,includeBounds:x}=s,v=o||1,S=g-1,{min:P,max:O}=i,z=!(0,u.k)(r),A=!(0,u.k)(l),B=!(0,u.k)(d),j=(O-P)/(m+1);let H,W,V,U,L=(0,u.aH)((O-P)/S/v)*v;if(L<1e-14&&!z&&!A)return[{value:P},{value:O}];U=Math.ceil(O/L)-Math.floor(P/L),U>S&&(L=(0,u.aH)(U*L/S/v)*v),(0,u.k)(c)||(H=Math.pow(10,c),L=Math.ceil(L*H)/H),"ticks"===n?(W=Math.floor(P/L)*L,V=Math.ceil(O/L)*L):(W=P,V=O),z&&A&&o&&(0,u.aI)((l-r)/o,L/1e3)?(U=Math.round(Math.min((l-r)/L,g)),L=(l-r)/U,W=r,V=l):B?(W=z?r:W,V=A?l:V,U=d-1,L=(V-W)/U):(U=(V-W)/L,U=(0,u.aJ)(U,Math.round(U),L/1e3)?Math.round(U):Math.ceil(U));const tt=Math.max((0,u.aK)(L),(0,u.aK)(W));H=Math.pow(10,(0,u.k)(c)?tt:c),W=Math.round(W*H)/H,V=Math.round(V*H)/H;let J=0;for(z&&(x&&W!==r?(t.push({value:r}),Wl)break;t.push({value:nt})}return A&&x&&V!==l?t.length&&(0,u.aJ)(t[t.length-1].value,l,ko(l,j,s))?t[t.length-1].value=l:t.push({value:l}):(!A||V===l)&&t.push({value:V}),t}({maxTicks:e,bounds:i.bounds,min:i.min,max:i.max,precision:t.precision,step:t.stepSize,count:t.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:t.minRotation||0,includeBounds:!1!==t.includeBounds},this._range||this);return"ticks"===i.bounds&&(0,u.aG)(r,this,"value"),i.reverse?(r.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),r}configure(){const i=this.ticks;let t=this.min,e=this.max;if(super.configure(),this.options.offset&&i.length){const n=(e-t)/Math.max(i.length-1,1)/2;t-=n,e+=n}this._startValue=t,this._endValue=e,this._valueRange=e-t}getLabelForValue(i){return(0,u.o)(i,this.chart.options.locale,this.options.ticks.format)}}class io extends es{determineDataLimits(){const{min:i,max:t}=this.getMinMax(!0);this.min=(0,u.g)(i)?i:0,this.max=(0,u.g)(t)?t:1,this.handleTickRangeOptions()}computeTickLimit(){const i=this.isHorizontal(),t=i?this.width:this.height,e=(0,u.t)(this.options.ticks.minRotation),n=(i?Math.sin(e):Math.cos(e))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(t/Math.min(40,o.lineHeight/n))}getPixelForValue(i){return null===i?NaN:this.getPixelForDecimal((i-this._startValue)/this._valueRange)}getValueForPixel(i){return this._startValue+this.getDecimalForPixel(i)*this._valueRange}}(0,R.Z)(io,"id","linear"),(0,R.Z)(io,"defaults",{ticks:{callback:u.aL.formatters.numeric}});const sn=s=>Math.floor((0,u.aM)(s)),Pt=(s,i)=>Math.pow(10,sn(s)+i);function is(s){return s/Math.pow(10,sn(s))==1}function Po(s,i,t){const e=Math.pow(10,t),n=Math.floor(s/e);return Math.ceil(i/e)-n}class no extends Nt{constructor(i){super(i),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(i,t){const e=es.prototype.parse.apply(this,[i,t]);if(0!==e)return(0,u.g)(e)&&e>0?e:null;this._zero=!0}determineDataLimits(){const{min:i,max:t}=this.getMinMax(!0);this.min=(0,u.g)(i)?Math.max(0,i):null,this.max=(0,u.g)(t)?Math.max(0,t):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!(0,u.g)(this._userMin)&&(this.min=i===Pt(this.min,0)?Pt(this.min,-1):Pt(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:i,maxDefined:t}=this.getUserBounds();let e=this.min,n=this.max;const o=l=>e=i?e:l,r=l=>n=t?n:l;e===n&&(e<=0?(o(1),r(10)):(o(Pt(e,-1)),r(Pt(n,1)))),e<=0&&o(Pt(n,-1)),n<=0&&r(Pt(e,1)),this.min=e,this.max=n}buildTicks(){const i=this.options,e=function Co(s,{min:i,max:t}){i=(0,u.O)(s.min,i);const e=[],n=sn(i);let o=function Ce(s,i){let e=sn(i-s);for(;Po(s,i,e)>10;)e++;for(;Po(s,i,e)<10;)e--;return Math.min(e,sn(s))}(i,t),r=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),c=n>o?Math.pow(10,n):0,d=Math.round((i-c)*r)/r,g=Math.floor((i-c)/l/10)*l*10;let m=Math.floor((d-g)/Math.pow(10,o)),x=(0,u.O)(s.min,Math.round((c+g+m*Math.pow(10,o))*r)/r);for(;x=10?m=m<15?15:20:m++,m>=20&&(o++,m=2,r=o>=0?1:r),x=Math.round((c+g+m*Math.pow(10,o))*r)/r;const v=(0,u.O)(s.max,x);return e.push({value:v,major:is(v),significand:m}),e}({min:this._userMin,max:this._userMax},this);return"ticks"===i.bounds&&(0,u.aG)(e,this,"value"),i.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(i){return void 0===i?"0":(0,u.o)(i,this.chart.options.locale,this.options.ticks.format)}configure(){const i=this.min;super.configure(),this._startValue=(0,u.aM)(i),this._valueRange=(0,u.aM)(this.max)-(0,u.aM)(i)}getPixelForValue(i){return(void 0===i||0===i)&&(i=this.min),null===i||isNaN(i)?NaN:this.getPixelForDecimal(i===this.min?0:((0,u.aM)(i)-this._startValue)/this._valueRange)}getValueForPixel(i){const t=this.getDecimalForPixel(i);return Math.pow(10,this._startValue+t*this._valueRange)}}function _(s){const i=s.ticks;if(i.display&&s.display){const t=(0,u.E)(i.backdropPadding);return(0,u.v)(i.font&&i.font.size,u.d.font.size)+t.height}return 0}function w(s,i,t){return t=(0,u.b)(t)?t:[t],{w:(0,u.aN)(s,i.string,t),h:t.length*i.lineHeight}}function D(s,i,t,e,n){return s===e||s===n?{start:i-t/2,end:i+t/2}:sn?{start:i-t,end:i}:{start:i,end:i+t}}function Q(s,i,t,e,n){const o=Math.abs(Math.sin(t)),r=Math.abs(Math.cos(t));let l=0,c=0;e.starti.r&&(l=(e.end-i.r)/o,s.r=Math.max(s.r,i.r+l)),n.starti.b&&(c=(n.end-i.b)/r,s.b=Math.max(s.b,i.b+c))}function st(s,i,t){const e=s.drawingArea,{extra:n,additionalAngle:o,padding:r,size:l}=t,c=s.getPointPosition(i,e+n+r,o),d=Math.round((0,u.U)((0,u.ay)(c.angle+u.H))),g=function Rt(s,i,t){return 90===t||270===t?s-=i/2:(t>270||t<90)&&(s-=i),s}(c.y,l.h,d),m=function ht(s){return 0===s||180===s?"center":s<180?"left":"right"}(d),x=function Wt(s,i,t){return"right"===t?s-=i:"center"===t&&(s-=i/2),s}(c.x,l.w,m);return{visible:!0,x:c.x,y:g,textAlign:m,left:x,top:g,right:x+l.w,bottom:g+l.h}}function it(s,i){if(!i)return!0;const{left:t,top:e,right:n,bottom:o}=s;return!((0,u.C)({x:t,y:e},i)||(0,u.C)({x:t,y:o},i)||(0,u.C)({x:n,y:e},i)||(0,u.C)({x:n,y:o},i))}function on(s,i,t){const{left:e,top:n,right:o,bottom:r}=t,{backdropColor:l}=i;if(!(0,u.k)(l)){const c=(0,u.aw)(i.borderRadius),d=(0,u.E)(i.backdropPadding);s.fillStyle=l;const g=e-d.left,m=n-d.top,x=o-e+d.width,v=r-n+d.height;Object.values(c).some(S=>0!==S)?(s.beginPath(),(0,u.au)(s,{x:g,y:m,w:x,h:v,radius:c}),s.fill()):s.fillRect(g,m,x,v)}}function ns(s,i,t,e){const{ctx:n}=s;if(t)n.arc(s.xCenter,s.yCenter,i,0,u.T);else{let o=s.getPointPosition(0,i);n.moveTo(o.x,o.y);for(let r=1;r{const n=(0,u.Q)(this.options.pointLabels.callback,[t,e],this);return n||0===n?n:""}).filter((t,e)=>this.chart.getDataVisibility(e))}fit(){const i=this.options;i.display&&i.pointLabels.display?function I(s){const i={l:s.left+s._padding.left,r:s.right-s._padding.right,t:s.top+s._padding.top,b:s.bottom-s._padding.bottom},t=Object.assign({},i),e=[],n=[],o=s._pointLabels.length,r=s.options.pointLabels,l=r.centerPointLabels?u.P/o:0;for(let c=0;c=0&&i=0;n--){const o=s._pointLabelItems[n];if(!o.visible)continue;const r=e.setContext(s.getPointLabelContext(n));on(t,r,o);const l=(0,u.a0)(r.font),{x:c,y:d,textAlign:g}=o;(0,u.Z)(t,s._pointLabels[n],c,d+l.lineHeight/2,l,{color:r.color,textAlign:g,textBaseline:"middle"})}}(this,r),n.display&&this.ticks.forEach((g,m)=>{if(0!==m){c=this.getDistanceFromCenterForValue(g.value);const x=this.getContext(m),v=n.setContext(x),S=o.setContext(x);!function Oo(s,i,t,e,n){const o=s.ctx,r=i.circular,{color:l,lineWidth:c}=i;!r&&!e||!l||!c||t<0||(o.save(),o.strokeStyle=l,o.lineWidth=c,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),ns(s,t,r,e),o.closePath(),o.stroke(),o.restore())}(this,v,c,r,S)}}),e.display){for(i.save(),l=r-1;l>=0;l--){const g=e.setContext(this.getPointLabelContext(l)),{color:m,lineWidth:x}=g;!x||!m||(i.lineWidth=x,i.strokeStyle=m,i.setLineDash(g.borderDash),i.lineDashOffset=g.borderDashOffset,c=this.getDistanceFromCenterForValue(t.ticks.reverse?this.min:this.max),d=this.getPointPosition(l,c),i.beginPath(),i.moveTo(this.xCenter,this.yCenter),i.lineTo(d.x,d.y),i.stroke())}i.restore()}}drawBorder(){}drawLabels(){const i=this.ctx,t=this.options,e=t.ticks;if(!e.display)return;const n=this.getIndexAngle(0);let o,r;i.save(),i.translate(this.xCenter,this.yCenter),i.rotate(n),i.textAlign="center",i.textBaseline="middle",this.ticks.forEach((l,c)=>{if(0===c&&!t.reverse)return;const d=e.setContext(this.getContext(c)),g=(0,u.a0)(d.font);if(o=this.getDistanceFromCenterForValue(this.ticks[c].value),d.showLabelBackdrop){i.font=g.string,r=i.measureText(l.label).width,i.fillStyle=d.backdropColor;const m=(0,u.E)(d.backdropPadding);i.fillRect(-r/2-m.left,-o-g.size/2-m.top,r+m.width,g.size+m.height)}(0,u.Z)(i,l.label,0,-o,g,{color:d.color,strokeColor:d.textStrokeColor,strokeWidth:d.textStrokeWidth})}),i.restore()}drawTitle(){}}(0,R.Z)(oo,"id","radialLinear"),(0,R.Z)(oo,"defaults",{display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:u.aL.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:s=>s,padding:5,centerPointLabels:!1}}),(0,R.Z)(oo,"defaultRoutes",{"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"}),(0,R.Z)(oo,"descriptors",{angleLines:{_fallback:"grid"}});const Do={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Yt=Object.keys(Do);function Go(s,i){return s-i}function Qo(s,i){if((0,u.k)(i))return null;const t=s._adapter,{parser:e,round:n,isoWeekday:o}=s._parseOpts;let r=i;return"function"==typeof e&&(r=e(r)),(0,u.g)(r)||(r="string"==typeof e?t.parse(r,e):t.parse(r)),null===r?null:(n&&(r="week"!==n||!(0,u.x)(o)&&!0!==o?t.startOf(r,n):t.startOf(r,"isoWeek",o)),+r)}function qo(s,i,t,e){const n=Yt.length;for(let o=Yt.indexOf(s);o=i?t[e]:t[n]]=!0}}else s[i]=!0}function tr(s,i,t){const e=[],n={},o=i.length;let r,l;for(r=0;r=0&&(i[c].major=!0);return i}(s,e,n,t):e}let Xo=(()=>{class s extends Nt{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const n=t.time||(t.time={}),o=this._adapter=new ls__date(t.adapters.date);o.init(e),(0,u.ab)(n.displayFormats,o.formats()),this._parseOpts={parser:n.parser,round:n.round,isoWeekday:n.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Qo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,n=t.time.unit||"day";let{min:o,max:r,minDefined:l,maxDefined:c}=this.getUserBounds();function d(g){!l&&!isNaN(g.min)&&(o=Math.min(o,g.min)),!c&&!isNaN(g.max)&&(r=Math.max(r,g.max))}(!l||!c)&&(d(this._getLabelBounds()),("ticks"!==t.bounds||"labels"!==t.ticks.source)&&d(this.getMinMax(!1))),o=(0,u.g)(o)&&!isNaN(o)?o:+e.startOf(Date.now(),n),r=(0,u.g)(r)&&!isNaN(r)?r:+e.endOf(Date.now(),n)+1,this.min=Math.min(o,r-1),this.max=Math.max(o+1,r)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,n=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],n=t[t.length-1]),{min:e,max:n}}buildTicks(){const t=this.options,e=t.time,n=t.ticks,o="labels"===n.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&o.length&&(this.min=this._userMin||o[0],this.max=this._userMax||o[o.length-1]);const r=this.min,c=(0,u.aO)(o,r,this.max);return this._unit=e.unit||(n.autoSkip?qo(e.minUnit,this.min,this.max,this._getLabelCapacity(r)):function ir(s,i,t,e,n){for(let o=Yt.length-1;o>=Yt.indexOf(t);o--){const r=Yt[o];if(Do[r].common&&s._adapter.diff(n,e,r)>=i-1)return r}return Yt[t?Yt.indexOf(t):0]}(this,c.length,e.minUnit,this.min,this.max)),this._majorUnit=n.major.enabled&&"year"!==this._unit?function nr(s){for(let i=Yt.indexOf(s)+1,t=Yt.length;i+t.value))}initOffsets(t=[]){let o,r,e=0,n=0;this.options.offset&&t.length&&(o=this.getDecimalForValue(t[0]),e=1===t.length?1-o:(this.getDecimalForValue(t[1])-o)/2,r=this.getDecimalForValue(t[t.length-1]),n=1===t.length?r:(r-this.getDecimalForValue(t[t.length-2]))/2);const l=t.length<3?.5:.25;e=(0,u.S)(e,0,l),n=(0,u.S)(n,0,l),this._offsets={start:e,end:n,factor:1/(e+1+n)}}_generate(){const t=this._adapter,e=this.min,n=this.max,o=this.options,r=o.time,l=r.unit||qo(r.minUnit,e,n,this._getLabelCapacity(e)),c=(0,u.v)(o.ticks.stepSize,1),d="week"===l&&r.isoWeekday,g=(0,u.x)(d)||!0===d,m={};let v,S,x=e;if(g&&(x=+t.startOf(x,"isoWeek",d)),x=+t.startOf(x,g?"day":l),t.diff(n,e,l)>1e5*c)throw new Error(e+" and "+n+" are too far apart with stepSize of "+c+" "+l);const P="data"===o.ticks.source&&this.getDataTimestamps();for(v=x,S=0;v+O)}getLabelForValue(t){const n=this.options.time;return this._adapter.format(t,n.tooltipFormat?n.tooltipFormat:n.displayFormats.datetime)}format(t,e){return this._adapter.format(t,e||this.options.time.displayFormats[this._unit])}_tickFormatFunction(t,e,n,o){const r=this.options,l=r.ticks.callback;if(l)return(0,u.Q)(l,[t,e,n],this);const c=r.time.displayFormats,d=this._unit,g=this._majorUnit,x=g&&c[g],v=n[e];return this._adapter.format(t,o||(g&&x&&v&&v.major?x:d&&c[d]))}generateTickLabels(t){let e,n,o;for(e=0,n=t.length;e0?c:1}getDataTimestamps(){let e,n,t=this._cache.data||[];if(t.length)return t;const o=this.getMatchingVisibleMetas();if(this._normalized&&o.length)return this._cache.data=o[0].controller.getAllParsedValues(this);for(e=0,n=o.length;e=s[e].pos&&i<=s[n].pos&&({lo:e,hi:n}=(0,u.B)(s,"pos",i)),({pos:o,time:l}=s[e]),({pos:r,time:c}=s[n])):(i>=s[e].time&&i<=s[n].time&&({lo:e,hi:n}=(0,u.B)(s,"time",i)),({time:o,pos:l}=s[e]),({time:r,pos:c}=s[n]));const d=r-o;return d?l+(c-l)*(i-o)/d:l}class Ko extends Xo{constructor(i){super(i),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const i=this._getTimestampsForTable(),t=this._table=this.buildLookupTable(i);this._minPos=Ao(t,this.min),this._tableRange=Ao(t,this.max)-this._minPos,super.initOffsets(i)}buildLookupTable(i){const{min:t,max:e}=this,n=[],o=[];let r,l,c,d,g;for(r=0,l=i.length;r=t&&d<=e&&n.push(d);if(n.length<2)return[{time:t,pos:0},{time:e,pos:1}];for(r=0,l=n.length;rn-o)}_getTimestampsForTable(){let i=this._cache.all||[];if(i.length)return i;const t=this.getDataTimestamps(),e=this.getLabelTimestamps();return i=t.length&&e.length?this.normalize(t.concat(e)):t.length?t:e,i=this._cache.all=i,i}getDecimalForValue(i){return(Ao(this._table,i)-this._minPos)/this._tableRange}getValueForPixel(i){const t=this._offsets,e=this.getDecimalForPixel(i)/t.factor-t.end;return Ao(this._table,e*this._tableRange+this._minPos,!0)}}(0,R.Z)(Ko,"id","timeseries"),(0,R.Z)(Ko,"defaults",Xo.defaults);const rr=[yt,yi,Vo,Object.freeze({__proto__:null,CategoryScale:Zo,LinearScale:io,LogarithmicScale:no,RadialLinearScale:oo,TimeScale:Xo,TimeSeriesScale:Ko})]},98137:(zo,wi,ct)=>{function R(a){return a+.5|0}ct.d(wi,{$:()=>fi,A:()=>ys,B:()=>xe,C:()=>Ie,D:()=>_s,E:()=>Tn,F:()=>cs,G:()=>Vn,H:()=>vt,I:()=>xi,J:()=>Ks,K:()=>Xs,L:()=>ri,M:()=>Ws,N:()=>cn,O:()=>uo,P:()=>ft,Q:()=>ls,R:()=>zn,S:()=>si,T:()=>Et,U:()=>bs,V:()=>ui,W:()=>xs,X:()=>Ts,Y:()=>wn,Z:()=>Ls,_:()=>oi,a:()=>Rs,a0:()=>Es,a1:()=>ks,a2:()=>fn,a3:()=>_n,a4:()=>be,a5:()=>Ri,a6:()=>Re,a7:()=>ze,a8:()=>ae,a9:()=>$i,aA:()=>qi,aB:()=>gn,aC:()=>qs,aD:()=>Sn,aE:()=>Fi,aF:()=>as,aG:()=>ps,aH:()=>fs,aI:()=>hn,aJ:()=>Bt,aK:()=>ni,aL:()=>Vi,aM:()=>_e,aN:()=>As,aO:()=>vs,aP:()=>jt,aa:()=>Ln,ab:()=>ei,ac:()=>zi,ad:()=>ws,ae:()=>Zs,af:()=>vn,ag:()=>ds,ah:()=>Ae,ai:()=>ln,aj:()=>dn,ak:()=>gi,al:()=>Vs,am:()=>Un,an:()=>tn,ao:()=>Wn,ap:()=>Yn,aq:()=>ce,ar:()=>Fe,as:()=>po,at:()=>Mn,au:()=>Cn,av:()=>He,aw:()=>Yi,ax:()=>Ji,ay:()=>Ht,az:()=>Qs,b:()=>_t,c:()=>Ps,d:()=>yn,e:()=>li,f:()=>re,g:()=>De,h:()=>hs,i:()=>xt,j:()=>je,k:()=>Oe,l:()=>Ms,m:()=>oe,n:()=>Je,o:()=>Vt,p:()=>Bi,q:()=>pn,r:()=>un,s:()=>Ii,t:()=>ms,u:()=>Ss,v:()=>yt,w:()=>ye,x:()=>gs,y:()=>Hs,z:()=>$s});const u=(a,h,f)=>Math.max(Math.min(a,f),h);function E(a){return u(R(2.55*a),0,255)}function St(a){return u(R(255*a),0,255)}function Dt(a){return u(R(a/2.55)/100,0,1)}function Ve(a){return u(R(100*a),0,100)}const mt={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Ut=[..."0123456789ABCDEF"],We=a=>Ut[15&a],ie=a=>Ut[(240&a)>>4]+Ut[15&a],de=a=>(240&a)>>4==(15&a);const Ue=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Ze(a,h,f){const p=h*Math.min(f,1-f),b=(y,M=(y+a/30)%12)=>f-p*Math.max(Math.min(M-3,9-M,1),-1);return[b(0),b(8),b(4)]}function Ci(a,h,f){const p=(b,y=(b+a/60)%6)=>f-f*h*Math.max(Math.min(y,4-y,1),0);return[p(5),p(3),p(1)]}function Oi(a,h,f){const p=Ze(a,1,.5);let b;for(h+f>1&&(b=1/(h+f),h*=b,f*=b),b=0;b<3;b++)p[b]*=1-h-f,p[b]+=h;return p}function At(a){const f=a.r/255,p=a.g/255,b=a.b/255,y=Math.max(f,p,b),M=Math.min(f,p,b),k=(y+M)/2;let C,T,F;return y!==M&&(F=y-M,T=k>.5?F/(2-y-M):F/(y+M),C=function Xe(a,h,f,p,b){return a===b?(h-f)/p+(ha<=.0031308?12.92*a:1.055*Math.pow(a,1/2.4)-.055,zt=a=>a<=.04045?a/12.92:Math.pow((a+.055)/1.055,2.4);function Lt(a,h,f){if(a){let p=At(a);p[h]=Math.max(0,Math.min(p[h]+p[h]*f,0===h?360:1)),p=ue(p),a.r=p[0],a.g=p[1],a.b=p[2]}}function qe(a,h){return a&&Object.assign(h||{},a)}function Ti(a){var h={r:0,g:0,b:0,a:255};return Array.isArray(a)?a.length>=3&&(h={r:a[0],g:a[1],b:a[2],a:255},a.length>3&&(h.a=St(a[3]))):(h=qe(a,{r:0,g:0,b:0,a:1})).a=St(h.a),h}function rs(a){return"r"===a.charAt(0)?function q(a){const h=X.exec(a);let p,b,y,f=255;if(h){if(h[7]!==p){const M=+h[7];f=h[8]?E(M):u(255*M,0,255)}return p=+h[1],b=+h[3],y=+h[5],p=255&(h[2]?E(p):u(p,0,255)),b=255&(h[4]?E(b):u(b,0,255)),y=255&(h[6]?E(y):u(y,0,255)),{r:p,g:b,b:y,a:f}}}(a):function fe(a){const h=Ue.exec(a);let p,f=255;if(!h)return;h[5]!==p&&(f=h[6]?E(+h[5]):St(+h[5]));const b=se(+h[2]),y=+h[3]/100,M=+h[4]/100;return p="hwb"===h[1]?function Di(a,h,f){return Tt(Oi,a,h,f)}(b,y,M):"hsv"===h[1]?function ne(a,h,f){return Tt(Ci,a,h,f)}(b,y,M):ue(b,y,M),{r:p[0],g:p[1],b:p[2],a:f}}(a)}class pe{constructor(h){if(h instanceof pe)return h;const f=typeof h;let p;"object"===f?p=Ti(h):"string"===f&&(p=function Ye(a){var f,h=a.length;return"#"===a[0]&&(4===h||5===h?f={r:255&17*mt[a[1]],g:255&17*mt[a[2]],b:255&17*mt[a[3]],a:5===h?17*mt[a[4]]:255}:(7===h||9===h)&&(f={r:mt[a[1]]<<4|mt[a[2]],g:mt[a[3]]<<4|mt[a[4]],b:mt[a[5]]<<4|mt[a[6]],a:9===h?mt[a[7]]<<4|mt[a[8]]:255})),f}(h)||function N(a){ge||(ge=function Ai(){const a={},h=Object.keys(Qe),f=Object.keys(Ge);let p,b,y,M,k;for(p=0;p>16&255,y>>8&255,255&y]}return a}(),ge.transparent=[0,0,0,0]);const h=ge[a.toLowerCase()];return h&&{r:h[0],g:h[1],b:h[2],a:4===h.length?h[3]:255}}(h)||rs(h)),this._rgb=p,this._valid=!!p}get valid(){return this._valid}get rgb(){var h=qe(this._rgb);return h&&(h.a=Dt(h.a)),h}set rgb(h){this._rgb=Ti(h)}rgbString(){return this._valid?function G(a){return a&&(a.a<255?`rgba(${a.r}, ${a.g}, ${a.b}, ${Dt(a.a)})`:`rgb(${a.r}, ${a.g}, ${a.b})`)}(this._rgb):void 0}hexString(){return this._valid?function Pi(a){var h=(a=>de(a.r)&&de(a.g)&&de(a.b)&&de(a.a))(a)?We:ie;return a?"#"+h(a.r)+h(a.g)+h(a.b)+((a,h)=>a<255?h(a):"")(a.a,h):void 0}(this._rgb):void 0}hslString(){return this._valid?function It(a){if(!a)return;const h=At(a),f=h[0],p=Ve(h[1]),b=Ve(h[2]);return a.a<255?`hsla(${f}, ${p}%, ${b}%, ${Dt(a.a)})`:`hsl(${f}, ${p}%, ${b}%)`}(this._rgb):void 0}mix(h,f){if(h){const p=this.rgb,b=h.rgb;let y;const M=f===y?.5:f,k=2*M-1,C=p.a-b.a,T=((k*C==-1?k:(k+C)/(1+k*C))+1)/2;y=1-T,p.r=255&T*p.r+y*b.r+.5,p.g=255&T*p.g+y*b.g+.5,p.b=255&T*p.b+y*b.b+.5,p.a=M*p.a+(1-M)*b.a,this.rgb=p}return this}interpolate(h,f){return h&&(this._rgb=function Zt(a,h,f){const p=zt(Dt(a.r)),b=zt(Dt(a.g)),y=zt(Dt(a.b));return{r:St(lt(p+f*(zt(Dt(h.r))-p))),g:St(lt(b+f*(zt(Dt(h.g))-b))),b:St(lt(y+f*(zt(Dt(h.b))-y))),a:a.a+f*(h.a-a.a)}}(this._rgb,h._rgb,f)),this}clone(){return new pe(this.rgb)}alpha(h){return this._rgb.a=St(h),this}clearer(h){return this._rgb.a*=1-h,this}greyscale(){const h=this._rgb,f=R(.3*h.r+.59*h.g+.11*h.b);return h.r=h.g=h.b=f,this}opaquer(h){return this._rgb.a*=1+h,this}negate(){const h=this._rgb;return h.r=255-h.r,h.g=255-h.g,h.b=255-h.b,this}lighten(h){return Lt(this._rgb,2,h),this}darken(h){return Lt(this._rgb,2,-h),this}saturate(h){return Lt(this._rgb,1,h),this}desaturate(h){return Lt(this._rgb,1,-h),this}rotate(h){return function Ke(a,h){var f=At(a);f[0]=se(f[0]+h),f=ue(f),a.r=f[0],a.g=f[1],a.b=f[2]}(this._rgb,h),this}}function as(){}const zi=(()=>{let a=0;return()=>a++})();function Oe(a){return null===a||typeof a>"u"}function _t(a){if(Array.isArray&&Array.isArray(a))return!0;const h=Object.prototype.toString.call(a);return"[object"===h.slice(0,7)&&"Array]"===h.slice(-6)}function xt(a){return null!==a&&"[object Object]"===Object.prototype.toString.call(a)}function De(a){return("number"==typeof a||a instanceof Number)&&isFinite(+a)}function uo(a,h){return De(a)?a:h}function yt(a,h){return typeof a>"u"?h:a}const oe=(a,h)=>"string"==typeof a&&a.endsWith("%")?parseFloat(a)/100:+a/h,Je=(a,h)=>"string"==typeof a&&a.endsWith("%")?parseFloat(a)/100*h:+a;function ls(a,h,f){if(a&&"function"==typeof a.call)return a.apply(f,h)}function cs(a,h,f,p){let b,y,M;if(_t(a))if(y=a.length,p)for(b=y-1;b>=0;b--)h.call(f,a[b],b);else for(b=0;ba,x:a=>a.x,y:a=>a.y};function re(a,h){return(Ei[h]||(Ei[h]=function an(a){const h=function Te(a){const h=a.split("."),f=[];let p="";for(const b of h)p+=b,p.endsWith("\\")?p=p.slice(0,-1)+".":(f.push(p),p="");return f}(a);return f=>{for(const p of h){if(""===p)break;f=f&&f[p]}return f}}(h)))(a)}function Ri(a){return a.charAt(0).toUpperCase()+a.slice(1)}const hs=a=>typeof a<"u",ze=a=>"function"==typeof a,ds=(a,h)=>{if(a.size!==h.size)return!1;for(const f of a)if(!h.has(f))return!1;return!0};function ln(a){return"mouseup"===a.type||"click"===a.type||"contextmenu"===a.type}const ft=Math.PI,Et=2*ft,us=Et+ft,ii=Number.POSITIVE_INFINITY,Le=ft/180,vt=ft/2,Ft=ft/4,gt=2*ft/3,_e=Math.log10,Ii=Math.sign;function Bt(a,h,f){return Math.abs(a-h)b-y).pop(),h}function gs(a){return!isNaN(parseFloat(a))&&isFinite(a)}function hn(a,h){const f=Math.round(a);return f-h<=a&&f+h>=a}function ps(a,h,f){let p,b,y;for(p=0,b=a.length;pC&&T=Math.min(h,f)-p&&a<=Math.max(h,f)+p}function jt(a,h,f){f=f||(M=>a[M]1;)y=b+p>>1,f(y)?b=y:p=y;return{lo:b,hi:p}}const xe=(a,h,f,p)=>jt(a,f,p?b=>{const y=a[b][h];return ya[b][h]jt(a,f,p=>a[p][h]>=f);function vs(a,h,f){let p=0,b=a.length;for(;pp&&a[b-1]>f;)b--;return p>0||b{const p="_onData"+Ri(f),b=a[f];Object.defineProperty(a,f,{configurable:!0,enumerable:!1,value(...y){const M=b.apply(this,y);return a._chartjs.listeners.forEach(k=>{"function"==typeof k[p]&&k[p](...y)}),M}})}))}function Ss(a,h){const f=a._chartjs;if(!f)return;const p=f.listeners,b=p.indexOf(h);-1!==b&&p.splice(b,1),!(p.length>0)&&(wt.forEach(y=>{delete a[y]}),delete a._chartjs)}function oi(a){const h=new Set(a);return h.size===a.length?a:Array.from(h)}const un=typeof window>"u"?function(a){return a()}:window.requestAnimationFrame;function ri(a,h){let f=[],p=!1;return function(...b){f=b,p||(p=!0,un.call(window,()=>{p=!1,a.apply(h,f)}))}}function ws(a,h){let f;return function(...p){return h?(clearTimeout(f),f=setTimeout(a,h,p)):a.apply(this,p),h}}const ks=a=>"start"===a?"left":"end"===a?"right":"center",fn=(a,h,f)=>"start"===a?h:"end"===a?f:(h+f)/2,gn=(a,h,f,p)=>a===(p?"left":"right")?f:"center"===a?(h+f)/2:h;function pn(a,h,f){const p=h.length;let b=0,y=p;if(a._sorted){const{iScale:M,_parsed:k}=a,C=M.axis,{min:T,max:F,minDefined:$,maxDefined:Z}=M.getUserBounds();$&&(b=si(Math.min(xe(k,C,T).lo,f?p:xe(h,C,M.getPixelForValue(T)).lo),0,p-1)),y=Z?si(Math.max(xe(k,M.axis,F,!0).hi+1,f?0:xe(h,C,M.getPixelForValue(F),!0).hi+1),b,p)-b:p-b}return{start:b,count:y}}function ye(a){const{xScale:h,yScale:f,_scaleRanges:p}=a,b={xmin:h.min,xmax:h.max,ymin:f.min,ymax:f.max};if(!p)return a._scaleRanges=b,!0;const y=p.xmin!==h.min||p.xmax!==h.max||p.ymin!==f.min||p.ymax!==f.max;return Object.assign(p,b),y}const ai=a=>0===a||1===a,ve=(a,h,f)=>-Math.pow(2,10*(a-=1))*Math.sin((a-h)*Et/f),Hi=(a,h,f)=>Math.pow(2,-10*a)*Math.sin((a-h)*Et/f)+1,li={linear:a=>a,easeInQuad:a=>a*a,easeOutQuad:a=>-a*(a-2),easeInOutQuad:a=>(a/=.5)<1?.5*a*a:-.5*(--a*(a-2)-1),easeInCubic:a=>a*a*a,easeOutCubic:a=>(a-=1)*a*a+1,easeInOutCubic:a=>(a/=.5)<1?.5*a*a*a:.5*((a-=2)*a*a+2),easeInQuart:a=>a*a*a*a,easeOutQuart:a=>-((a-=1)*a*a*a-1),easeInOutQuart:a=>(a/=.5)<1?.5*a*a*a*a:-.5*((a-=2)*a*a*a-2),easeInQuint:a=>a*a*a*a*a,easeOutQuint:a=>(a-=1)*a*a*a*a+1,easeInOutQuint:a=>(a/=.5)<1?.5*a*a*a*a*a:.5*((a-=2)*a*a*a*a+2),easeInSine:a=>1-Math.cos(a*vt),easeOutSine:a=>Math.sin(a*vt),easeInOutSine:a=>-.5*(Math.cos(ft*a)-1),easeInExpo:a=>0===a?0:Math.pow(2,10*(a-1)),easeOutExpo:a=>1===a?1:1-Math.pow(2,-10*a),easeInOutExpo:a=>ai(a)?a:a<.5?.5*Math.pow(2,10*(2*a-1)):.5*(2-Math.pow(2,-10*(2*a-1))),easeInCirc:a=>a>=1?a:-(Math.sqrt(1-a*a)-1),easeOutCirc:a=>Math.sqrt(1-(a-=1)*a),easeInOutCirc:a=>(a/=.5)<1?-.5*(Math.sqrt(1-a*a)-1):.5*(Math.sqrt(1-(a-=2)*a)+1),easeInElastic:a=>ai(a)?a:ve(a,.075,.3),easeOutElastic:a=>ai(a)?a:Hi(a,.075,.3),easeInOutElastic:a=>ai(a)?a:a<.5?.5*ve(2*a,.1125,.45):.5+.5*Hi(2*a-1,.1125,.45),easeInBack:a=>a*a*(2.70158*a-1.70158),easeOutBack:a=>(a-=1)*a*(2.70158*a+1.70158)+1,easeInOutBack(a){let h=1.70158;return(a/=.5)<1?a*a*((1+(h*=1.525))*a-h)*.5:.5*((a-=2)*a*((1+(h*=1.525))*a+h)+2)},easeInBounce:a=>1-li.easeOutBounce(1-a),easeOutBounce:a=>a<1/2.75?7.5625*a*a:a<2/2.75?7.5625*(a-=1.5/2.75)*a+.75:a<2.5/2.75?7.5625*(a-=2.25/2.75)*a+.9375:7.5625*(a-=2.625/2.75)*a+.984375,easeInOutBounce:a=>a<.5?.5*li.easeInBounce(2*a):.5*li.easeOutBounce(2*a-1)+.5};function Xt(a){if(a&&"object"==typeof a){const h=a.toString();return"[object CanvasPattern]"===h||"[object CanvasGradient]"===h}return!1}function Ps(a){return Xt(a)?a:new pe(a)}function ji(a){return Xt(a)?a:new pe(a).saturate(.5).darken(.1).hexString()}const Nt=["x","y","borderWidth","radius","tension"],ci=["color","borderColor","backgroundColor"],hi=new Map;function Vt(a,h,f){return function Os(a,h){h=h||{};const f=a+JSON.stringify(h);let p=hi.get(f);return p||(p=new Intl.NumberFormat(a,h),hi.set(f,p)),p}(h,f).format(a)}const mn={values:a=>_t(a)?a:""+a,numeric(a,h,f){if(0===a)return"0";const p=this.chart.options.locale;let b,y=a;if(f.length>1){const T=Math.max(Math.abs(f[0].value),Math.abs(f[f.length-1].value));(T<1e-4||T>1e15)&&(b="scientific"),y=function Ds(a,h){let f=h.length>3?h[2].value-h[1].value:h[1].value-h[0].value;return Math.abs(f)>=1&&a!==Math.floor(a)&&(f=a-Math.floor(a)),f}(a,f)}const M=_e(Math.abs(y)),k=isNaN(M)?1:Math.max(Math.min(-1*Math.floor(M),20),0),C={notation:b,minimumFractionDigits:k,maximumFractionDigits:k};return Object.assign(C,this.options.ticks.format),Vt(a,p,C)},logarithmic(a,h,f){if(0===a)return"0";const p=f[h].significand||a/Math.pow(10,Math.floor(_e(a)));return[1,2,3,5,10,15].includes(p)||h>.8*f.length?mn.numeric.call(this,a,h,f):""}};var Vi={formatters:mn};const _n=Object.create(null),Re=Object.create(null);function Me(a,h){if(!h)return a;const f=h.split(".");for(let p=0,b=f.length;pp.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(p,b)=>ji(b.backgroundColor),this.hoverBorderColor=(p,b)=>ji(b.borderColor),this.hoverColor=(p,b)=>ji(b.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(h),this.apply(f)}set(h,f){return Wi(this,h,f)}get(h){return Me(this,h)}describe(h,f){return Wi(Re,h,f)}override(h,f){return Wi(_n,h,f)}route(h,f,p,b){const y=Me(this,h),M=Me(this,p),k="_"+f;Object.defineProperties(y,{[k]:{value:y[f],writable:!0},[f]:{enumerable:!0,get(){const C=this[k],T=M[b];return xt(C)?Object.assign({},T,C):yt(C,T)},set(C){this[k]=C}}})}apply(h){h.forEach(f=>f(this))}}var yn=new xn({_scriptable:a=>!a.startsWith("on"),_indexable:a=>"events"!==a,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function Ni(a){a.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),a.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:h=>"onProgress"!==h&&"onComplete"!==h&&"fn"!==h}),a.set("animations",{colors:{type:"color",properties:ci},numbers:{type:"number",properties:Nt}}),a.describe("animations",{_fallback:"animation"}),a.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:h=>0|h}}}})},function Cs(a){a.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function bn(a){a.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(h,f)=>f.lineWidth,tickColor:(h,f)=>f.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Vi.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),a.route("scale.ticks","color","","color"),a.route("scale.grid","color","","borderColor"),a.route("scale.border","color","","borderColor"),a.route("scale.title","color","","color"),a.describe("scale",{_fallback:!1,_scriptable:h=>!h.startsWith("before")&&!h.startsWith("after")&&"callback"!==h&&"parser"!==h,_indexable:h=>"borderDash"!==h&&"tickBorderDash"!==h&&"dash"!==h}),a.describe("scales",{_fallback:"scale"}),a.describe("scale.ticks",{_scriptable:h=>"backdropPadding"!==h&&"callback"!==h,_indexable:h=>"backdropPadding"!==h})}]);function ui(a,h,f,p,b){let y=h[b];return y||(y=h[b]=a.measureText(b).width,f.push(b)),y>p&&(p=y),p}function As(a,h,f,p){let b=(p=p||{}).data=p.data||{},y=p.garbageCollect=p.garbageCollect||[];p.font!==h&&(b=p.data={},y=p.garbageCollect=[],p.font=h),a.save(),a.font=h;let M=0;const k=f.length;let C,T,F,$,Z;for(C=0;Cf.length){for(C=0;C0&&a.stroke()}}function Ie(a,h,f){return f=f||.5,!h||a&&a.x>h.left-f&&a.xh.top-f&&a.y0&&""!==y.strokeColor;let C,T;for(a.save(),a.font=b.string,function kn(a,h){h.translation&&a.translate(h.translation[0],h.translation[1]),Oe(h.rotation)||a.rotate(h.rotation),h.color&&(a.fillStyle=h.color),h.textAlign&&(a.textAlign=h.textAlign),h.textBaseline&&(a.textBaseline=h.textBaseline)}(a,y),C=0;C+a||0;function gi(a,h){const f={},p=xt(h),b=p?Object.keys(h):h,y=xt(a)?p?M=>yt(a[M],a[h[M]]):M=>a[M]:()=>a;for(const M of b)f[M]=Be(y(M));return f}function He(a){return gi(a,{top:"y",right:"x",bottom:"y",left:"x"})}function Yi(a){return gi(a,["topLeft","topRight","bottomLeft","bottomRight"])}function Tn(a){const h=He(a);return h.width=h.left+h.right,h.height=h.top+h.bottom,h}function Es(a,h){let f=yt((a=a||{}).size,(h=h||yn.font).size);"string"==typeof f&&(f=parseInt(f,10));let p=yt(a.style,h.style);p&&!(""+p).match(Dn)&&(console.warn('Invalid font style specified: "'+p+'"'),p=void 0);const b={family:yt(a.family,h.family),lineHeight:An(yt(a.lineHeight,h.lineHeight),f),size:f,style:p,weight:yt(a.weight,h.weight),string:""};return b.string=function di(a){return!a||Oe(a.size)||Oe(a.family)?null:(a.style?a.style+" ":"")+(a.weight?a.weight+" ":"")+a.size+"px "+a.family}(b),b}function Rs(a,h,f,p){let y,M,k,b=!0;for(y=0,M=a.length;yf&&0===k?0:k+C;return{min:M(p,-Math.abs(y)),max:M(b,y)}}function je(a,h){return Object.assign(Object.create(a),h)}function $i(a,h=[""],f,p,b=(()=>a[0])){const y=f||a;typeof p>"u"&&(p=Bn("_fallback",a));const M={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:a,_rootScopes:y,_fallback:p,_getTarget:b,override:k=>$i([k,...a],h,y,p)};return new Proxy(M,{deleteProperty:(k,C)=>(delete k[C],delete k._keys,delete a[0][C],!0),get:(k,C)=>En(k,C,()=>function Fs(a,h,f,p){let b;for(const y of h)if(b=Bn(Se(y,a),f),typeof b<"u")return le(a,b)?pi(f,p,a,b):b}(C,h,a,k)),getOwnPropertyDescriptor:(k,C)=>Reflect.getOwnPropertyDescriptor(k._scopes[0],C),getPrototypeOf:()=>Reflect.getPrototypeOf(a[0]),has:(k,C)=>Xi(k).includes(C),ownKeys:k=>Xi(k),set(k,C,T){const F=k._storage||(k._storage=b());return k[C]=F[C]=T,delete k._keys,!0}})}function ae(a,h,f,p){const b={_cacheable:!1,_proxy:a,_context:h,_subProxy:f,_stack:new Set,_descriptors:Ln(a,p),setContext:y=>ae(a,y,f,p),override:y=>ae(a.override(y),h,f,p)};return new Proxy(b,{deleteProperty:(y,M)=>(delete y[M],delete a[M],!0),get:(y,M,k)=>En(y,M,()=>function Ui(a,h,f){const{_proxy:p,_context:b,_subProxy:y,_descriptors:M}=a;let k=p[h];return ze(k)&&M.isScriptable(h)&&(k=function we(a,h,f,p){const{_proxy:b,_context:y,_subProxy:M,_stack:k}=f;if(k.has(a))throw new Error("Recursion detected: "+Array.from(k).join("->")+"->"+a);k.add(a);let C=h(y,M||p);return k.delete(a),le(a,C)&&(C=pi(b._scopes,b,a,C)),C}(h,k,a,f)),_t(k)&&k.length&&(k=function Rn(a,h,f,p){const{_proxy:b,_context:y,_subProxy:M,_descriptors:k}=f;if(typeof y.index<"u"&&p(a))return h[y.index%h.length];if(xt(h[0])){const C=h,T=b._scopes.filter(F=>F!==C);h=[];for(const F of C){const $=pi(T,b,a,F);h.push(ae($,y,M&&M[a],k))}}return h}(h,k,a,M.isIndexable)),le(h,k)&&(k=ae(k,b,y&&y[h],M)),k}(y,M,k)),getOwnPropertyDescriptor:(y,M)=>y._descriptors.allKeys?Reflect.has(a,M)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(a,M),getPrototypeOf:()=>Reflect.getPrototypeOf(a),has:(y,M)=>Reflect.has(a,M),ownKeys:()=>Reflect.ownKeys(a),set:(y,M,k)=>(a[M]=k,delete y[M],!0)})}function Ln(a,h={scriptable:!0,indexable:!0}){const{_scriptable:f=h.scriptable,_indexable:p=h.indexable,_allKeys:b=h.allKeys}=a;return{allKeys:b,scriptable:f,indexable:p,isScriptable:ze(f)?f:()=>f,isIndexable:ze(p)?p:()=>p}}const Se=(a,h)=>a?a+Ri(h):h,le=(a,h)=>xt(h)&&"adapters"!==a&&(null===Object.getPrototypeOf(h)||h.constructor===Object);function En(a,h,f){if(Object.prototype.hasOwnProperty.call(a,h))return a[h];const p=f();return a[h]=p,p}function In(a,h,f){return ze(a)?a(h,f):a}const Is=(a,h)=>!0===a?h:"string"==typeof a?re(h,a):void 0;function Zi(a,h,f,p,b){for(const y of h){const M=Is(f,y);if(M){a.add(M);const k=In(M._fallback,f,b);if(typeof k<"u"&&k!==f&&k!==p)return k}else if(!1===M&&typeof p<"u"&&f!==p)return null}return!1}function pi(a,h,f,p){const b=h._rootScopes,y=In(h._fallback,f,p),M=[...a,...b],k=new Set;k.add(p);let C=Fn(k,M,f,y||f,p);return!(null===C||typeof y<"u"&&y!==f&&(C=Fn(k,M,y,C,p),null===C))&&$i(Array.from(k),[""],b,y,()=>function mi(a,h,f){const p=a._getTarget();h in p||(p[h]={});const b=p[h];return _t(b)&&xt(f)?f:b||{}}(h,f,p))}function Fn(a,h,f,p,b){for(;f;)f=Zi(a,h,f,p,b);return f}function Bn(a,h){for(const f of h){if(!f)continue;const p=f[a];if(typeof p<"u")return p}}function Xi(a){let h=a._keys;return h||(h=a._keys=function Bs(a){const h=new Set;for(const f of a)for(const p of Object.keys(f).filter(b=>!b.startsWith("_")))h.add(p);return Array.from(h)}(a._scopes)),h}function Hs(a,h,f,p){const{iScale:b}=a,{key:y="r"}=this._parsing,M=new Array(p);let k,C,T,F;for(k=0,C=p;kh"x"===a?"y":"x";function Hn(a,h,f,p){const b=a.skip?h:a,y=h,M=f.skip?h:f,k=Fi(y,b),C=Fi(M,y);let T=k/(k+C),F=C/(k+C);T=isNaN(T)?0:T,F=isNaN(F)?0:F;const $=p*T,Z=p*F;return{previous:{x:y.x-$*(M.x-b.x),y:y.y-$*(M.y-b.y)},next:{x:y.x+Z*(M.x-b.x),y:y.y+Z*(M.y-b.y)}}}function _i(a,h,f){return Math.max(Math.min(a,f),h)}function Vs(a,h,f,p,b){let y,M,k,C;if(h.spanGaps&&(a=a.filter(T=>!T.skip)),"monotone"===h.cubicInterpolationMode)!function Ns(a,h="x"){const f=Ki(h),p=a.length,b=Array(p).fill(0),y=Array(p);let M,k,C,T=Kt(a,0);for(M=0;Ma.ownerDocument.defaultView.getComputedStyle(a,null),jn=["top","right","bottom","left"];function qt(a,h,f){const p={};f=f?"-"+f:"";for(let b=0;b<4;b++){const y=jn[b];p[y]=parseFloat(a[h+"-"+y+f])||0}return p.width=p.left+p.right,p.height=p.top+p.bottom,p}const Nn=(a,h,f)=>(a>0||h>0)&&(!f||!f.shadowRoot);function $s(a,h){if("native"in a)return a;const{canvas:f,currentDevicePixelRatio:p}=h,b=yi(f),y="border-box"===b.boxSizing,M=qt(b,"padding"),k=qt(b,"border","width"),{x:C,y:T,box:F}=function Ys(a,h){const f=a.touches,p=f&&f.length?f[0]:a,{offsetX:b,offsetY:y}=p;let k,C,M=!1;if(Nn(b,y,a.target))k=b,C=y;else{const T=h.getBoundingClientRect();k=p.clientX-T.left,C=p.clientY-T.top,M=!0}return{x:k,y:C,box:M}}(a,f),$=M.left+(F&&k.left),Z=M.top+(F&&k.top);let{width:ot,height:dt}=h;return y&&(ot-=M.width+k.width,dt-=M.height+k.height),{x:Math.round((C-$)/ot*f.width/p),y:Math.round((T-Z)/dt*f.height/p)}}const vi=a=>Math.round(10*a)/10;function Vn(a,h,f,p){const b=yi(a),y=qt(b,"margin"),M=Qt(b.maxWidth,a,"clientWidth")||ii,k=Qt(b.maxHeight,a,"clientHeight")||ii,C=function Us(a,h,f){let p,b;if(void 0===h||void 0===f){const y=xi(a);if(y){const M=y.getBoundingClientRect(),k=yi(y),C=qt(k,"border","width"),T=qt(k,"padding");h=M.width-T.width-C.width,f=M.height-T.height-C.height,p=Qt(k.maxWidth,y,"clientWidth"),b=Qt(k.maxHeight,y,"clientHeight")}else h=a.clientWidth,f=a.clientHeight}return{width:h,height:f,maxWidth:p||ii,maxHeight:b||ii}}(a,h,f);let{width:T,height:F}=C;if("content-box"===b.boxSizing){const Z=qt(b,"border","width"),ot=qt(b,"padding");T-=ot.width+Z.width,F-=ot.height+Z.height}return T=Math.max(0,T-y.width),F=Math.max(0,p?T/p:F-y.height),T=vi(Math.min(T,M,C.maxWidth)),F=vi(Math.min(F,k,C.maxHeight)),T&&!F&&(F=vi(T/2)),(void 0!==h||void 0!==f)&&p&&C.height&&F>C.height&&(F=C.height,T=vi(Math.floor(F*p))),{width:T,height:F}}function Zs(a,h,f){const p=h||1,b=Math.floor(a.height*p),y=Math.floor(a.width*p);a.height=Math.floor(a.height),a.width=Math.floor(a.width);const M=a.canvas;return M.style&&(f||!M.style.height&&!M.style.width)&&(M.style.height=`${a.height}px`,M.style.width=`${a.width}px`),(a.currentDevicePixelRatio!==p||M.height!==b||M.width!==y)&&(a.currentDevicePixelRatio=p,M.height=b,M.width=y,a.ctx.setTransform(p,0,0,p,0,0),!0)}const Xs=function(){let a=!1;try{const h={get passive(){return a=!0,!1}};window.addEventListener("test",null,h),window.removeEventListener("test",null,h)}catch{}return a}();function Ks(a,h){const f=function Qi(a,h){return yi(a).getPropertyValue(h)}(a,h),p=f&&f.match(/^(\d+)(\.\d+)?px$/);return p?+p[1]:void 0}function ce(a,h,f,p){return{x:a.x+f*(h.x-a.x),y:a.y+f*(h.y-a.y)}}function Wn(a,h,f,p){return{x:a.x+f*(h.x-a.x),y:"middle"===p?f<.5?a.y:h.y:"after"===p?f<1?a.y:h.y:f>0?h.y:a.y}}function Yn(a,h,f,p){const b={x:a.cp2x,y:a.cp2y},y={x:h.cp1x,y:h.cp1y},M=ce(a,b,f),k=ce(b,y,f),C=ce(y,h,f),T=ce(M,k,f),F=ce(k,C,f);return ce(T,F,f)}const Gs=function(a,h){return{x:f=>a+a+h-f,setWidth(f){h=f},textAlign:f=>"center"===f?f:"right"===f?"left":"right",xPlus:(f,p)=>f-p,leftForLtr:(f,p)=>f-p}},rt=function(){return{x:a=>a,setWidth(a){},textAlign:a=>a,xPlus:(a,h)=>a+h,leftForLtr:(a,h)=>a}};function Qs(a,h,f){return a?Gs(h,f):rt()}function qi(a,h){let f,p;("ltr"===h||"rtl"===h)&&(f=a.canvas.style,p=[f.getPropertyValue("direction"),f.getPropertyPriority("direction")],f.setProperty("direction",h,"important"),a.prevTextDirection=p)}function qs(a,h){void 0!==h&&(delete a.prevTextDirection,a.canvas.style.setProperty("direction",h[0],h[1]))}function Mi(a){return"angle"===a?{between:Bi,compare:Ee,normalize:Ht}:{between:dn,compare:(h,f)=>h-f,normalize:h=>h}}function Si({start:a,end:h,count:f,loop:p,style:b}){return{start:a%f,end:h%f,loop:p&&(h-a+1)%f==0,style:b}}function Ji(a,h,f){if(!f)return[a];const{property:p,start:b,end:y}=f,M=h.length,{compare:k,between:C,normalize:T}=Mi(p),{start:F,end:$,loop:Z,style:ot}=function ke(a,h,f){const{property:p,start:b,end:y}=f,{between:M,normalize:k}=Mi(p),C=h.length;let Z,ot,{start:T,end:F,loop:$}=a;if($){for(T+=C,F+=C,Z=0,ot=C;Zb&&a[y%h].skip;)y--;return y%=h,{start:b,end:y}}(f,b,y,p);return Zn(a,!0===p?[{start:M,end:k,loop:y}]:function $n(a,h,f,p){const b=a.length,y=[];let C,M=h,k=a[h];for(C=h+1;C<=f;++C){const T=a[C%b];T.skip||T.stop?k.skip||(y.push({start:h%b,end:(C-1)%b,loop:p=!1}),h=M=T.stop?C:null):(M=C,k.skip&&(h=C)),k=T}return null!==M&&y.push({start:h%b,end:M%b,loop:p}),y}(f,M,k{ct.d(wi,{$:()=>fe,$O:()=>Qe,Jp:()=>se,KJ:()=>ge,u9:()=>Tt,yG:()=>ue});var R=ct(49388),u=ct(96814),E=ct(65879),Ot=ct(8324),St=ct(62595),Dt=ct(97582),Ve=ct(78645),mt=ct(59773),Ut=ct(37398),We=ct(40874),ie=ct(1608),de=ct(28802);function ki(N,X){if(1&N&&(E.ynx(0),E._UZ(1,"span",9),E.BQk()),2&N){const q=X.$implicit,G=E.oxw(2);E.xp6(1),E.Q6J("nzType",q||G.getBackIcon())}}function Ye(N,X){if(1&N){const q=E.EpF();E.TgZ(0,"div",6),E.NdJ("click",function(){E.CHM(q);const lt=E.oxw();return E.KtG(lt.onBack())}),E.TgZ(1,"div",7),E.YNc(2,ki,2,1,"ng-container",8),E.qZA()()}if(2&N){const q=E.oxw();E.xp6(2),E.Q6J("nzStringTemplateOutlet",q.nzBackIcon)}}function $e(N,X){if(1&N&&(E.ynx(0),E._uU(1),E.BQk()),2&N){const q=E.oxw(2);E.xp6(1),E.Oqu(q.nzTitle)}}function Pi(N,X){if(1&N&&(E.TgZ(0,"span",10),E.YNc(1,$e,2,1,"ng-container",8),E.qZA()),2&N){const q=E.oxw();E.xp6(1),E.Q6J("nzStringTemplateOutlet",q.nzTitle)}}function Ue(N,X){1&N&&E.Hsn(0,6,["*ngIf","!nzTitle"])}function Ze(N,X){if(1&N&&(E.ynx(0),E._uU(1),E.BQk()),2&N){const q=E.oxw(2);E.xp6(1),E.Oqu(q.nzSubtitle)}}function Ci(N,X){if(1&N&&(E.TgZ(0,"span",11),E.YNc(1,Ze,2,1,"ng-container",8),E.qZA()),2&N){const q=E.oxw();E.xp6(1),E.Q6J("nzStringTemplateOutlet",q.nzSubtitle)}}function Oi(N,X){1&N&&E.Hsn(0,7,["*ngIf","!nzSubtitle"])}const Xe=[[["nz-breadcrumb","nz-page-header-breadcrumb",""]],[["nz-avatar","nz-page-header-avatar",""]],[["nz-page-header-tags"],["","nz-page-header-tags",""]],[["nz-page-header-extra"],["","nz-page-header-extra",""]],[["nz-page-header-content"],["","nz-page-header-content",""]],[["nz-page-header-footer"],["","nz-page-header-footer",""]],[["nz-page-header-title"],["","nz-page-header-title",""]],[["nz-page-header-subtitle"],["","nz-page-header-subtitle",""]]],At=["nz-breadcrumb[nz-page-header-breadcrumb]","nz-avatar[nz-page-header-avatar]","nz-page-header-tags, [nz-page-header-tags]","nz-page-header-extra, [nz-page-header-extra]","nz-page-header-content, [nz-page-header-content]","nz-page-header-footer, [nz-page-header-footer]","nz-page-header-title, [nz-page-header-title]","nz-page-header-subtitle, [nz-page-header-subtitle]"];let Tt=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275dir=E.lG2({type:N,selectors:[["nz-page-header-title"],["","nz-page-header-title",""]],hostAttrs:[1,"ant-page-header-heading-title"],exportAs:["nzPageHeaderTitle"]}),X})(),ue=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275dir=E.lG2({type:N,selectors:[["nz-page-header-subtitle"],["","nz-page-header-subtitle",""]],hostAttrs:[1,"ant-page-header-heading-sub-title"],exportAs:["nzPageHeaderSubtitle"]}),X})(),se=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275dir=E.lG2({type:N,selectors:[["nz-page-header-extra"],["","nz-page-header-extra",""]],hostAttrs:[1,"ant-page-header-heading-extra"],exportAs:["nzPageHeaderExtra"]}),X})(),fe=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275dir=E.lG2({type:N,selectors:[["nz-page-header-footer"],["","nz-page-header-footer",""]],hostAttrs:[1,"ant-page-header-footer"],exportAs:["nzPageHeaderFooter"]}),X})(),Ke=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275dir=E.lG2({type:N,selectors:[["nz-breadcrumb","nz-page-header-breadcrumb",""]],exportAs:["nzPageHeaderBreadcrumb"]}),X})(),Qe=(()=>{var N;class X{constructor(G,lt,zt,Zt,Lt,qe){this.location=G,this.nzConfigService=lt,this.elementRef=zt,this.nzResizeObserver=Zt,this.cdr=Lt,this.directionality=qe,this._nzModuleName="pageHeader",this.nzBackIcon=null,this.nzGhost=!0,this.nzBack=new E.vpe,this.compact=!1,this.destroy$=new Ve.x,this.dir="ltr"}ngOnInit(){this.directionality.change?.pipe((0,mt.R)(this.destroy$)).subscribe(G=>{this.dir=G,this.cdr.detectChanges()}),this.dir=this.directionality.value}ngAfterViewInit(){this.nzResizeObserver.observe(this.elementRef).pipe((0,Ut.U)(([G])=>G.contentRect.width),(0,mt.R)(this.destroy$)).subscribe(G=>{this.compact=G<768,this.cdr.markForCheck()})}onBack(){if(this.nzBack.observers.length)this.nzBack.emit();else{if(!this.location)throw new Error(`${ie.Bq} you should import 'RouterModule' or register 'Location' if you want to use 'nzBack' default event!`);this.location.back()}}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}getBackIcon(){return"rtl"===this.dir?"arrow-right":"arrow-left"}}return(N=X).\u0275fac=function(G){return new(G||N)(E.Y36(u.Ye,8),E.Y36(We.jY),E.Y36(E.SBq),E.Y36(de.D3),E.Y36(E.sBO),E.Y36(R.Is,8))},N.\u0275cmp=E.Xpm({type:N,selectors:[["nz-page-header"]],contentQueries:function(G,lt,zt){if(1&G&&(E.Suo(zt,fe,5),E.Suo(zt,Ke,5)),2&G){let Zt;E.iGM(Zt=E.CRH())&&(lt.nzPageHeaderFooter=Zt.first),E.iGM(Zt=E.CRH())&&(lt.nzPageHeaderBreadcrumb=Zt.first)}},hostAttrs:[1,"ant-page-header"],hostVars:10,hostBindings:function(G,lt){2&G&&E.ekj("has-footer",lt.nzPageHeaderFooter)("ant-page-header-ghost",lt.nzGhost)("has-breadcrumb",lt.nzPageHeaderBreadcrumb)("ant-page-header-compact",lt.compact)("ant-page-header-rtl","rtl"===lt.dir)},inputs:{nzBackIcon:"nzBackIcon",nzTitle:"nzTitle",nzSubtitle:"nzSubtitle",nzGhost:"nzGhost"},outputs:{nzBack:"nzBack"},exportAs:["nzPageHeader"],ngContentSelectors:At,decls:13,vars:5,consts:[[1,"ant-page-header-heading"],[1,"ant-page-header-heading-left"],["class","ant-page-header-back",3,"click",4,"ngIf"],["class","ant-page-header-heading-title",4,"ngIf"],[4,"ngIf"],["class","ant-page-header-heading-sub-title",4,"ngIf"],[1,"ant-page-header-back",3,"click"],["role","button","tabindex","0",1,"ant-page-header-back-button"],[4,"nzStringTemplateOutlet"],["nz-icon","","nzTheme","outline",3,"nzType"],[1,"ant-page-header-heading-title"],[1,"ant-page-header-heading-sub-title"]],template:function(G,lt){1&G&&(E.F$t(Xe),E.Hsn(0),E.TgZ(1,"div",0)(2,"div",1),E.YNc(3,Ye,3,1,"div",2),E.Hsn(4,1),E.YNc(5,Pi,2,1,"span",3),E.YNc(6,Ue,1,0,"ng-content",4),E.YNc(7,Ci,2,1,"span",5),E.YNc(8,Oi,1,0,"ng-content",4),E.Hsn(9,2),E.qZA(),E.Hsn(10,3),E.qZA(),E.Hsn(11,4),E.Hsn(12,5)),2&G&&(E.xp6(3),E.Q6J("ngIf",null!==lt.nzBackIcon),E.xp6(2),E.Q6J("ngIf",lt.nzTitle),E.xp6(1),E.Q6J("ngIf",!lt.nzTitle),E.xp6(1),E.Q6J("ngIf",lt.nzSubtitle),E.xp6(1),E.Q6J("ngIf",!lt.nzSubtitle))},dependencies:[u.O5,Ot.f,St.Ls],encapsulation:2,changeDetection:0}),(0,Dt.gn)([(0,We.oS)()],X.prototype,"nzGhost",void 0),X})(),ge=(()=>{var N;class X{}return(N=X).\u0275fac=function(G){return new(G||N)},N.\u0275mod=E.oAB({type:N}),N.\u0275inj=E.cJS({imports:[R.vT,u.ez,Ot.T,St.PV]}),X})()},47246:(zo,wi,ct)=>{ct.d(wi,{jh:()=>es,vQ:()=>no});var R=ct(65879),u=ct(17816),E=ct(98137),Ot=ct(65619),St=ct(93997);const Ut=function mt(_,w){return _===w||_!=_&&w!=w},ie=function We(_,w){for(var D=_.length;D--;)if(Ut(_[D][0],w))return D;return-1};var ki=Array.prototype.splice;function At(_){var w=-1,D=null==_?0:_.length;for(this.clear();++w-1},At.prototype.set=function Oi(_,w){var D=this.__data__,I=ie(D,_);return I<0?(++this.size,D.push([_,w])):D[I][1]=w,this};const Tt=At,Ai="object"==typeof global&&global&&global.Object===Object&&global;var ge="object"==typeof self&&self&&self.Object===Object&&self;const X=Ai||ge||Function("return this")();var q=X.Symbol,lt=Object.prototype,zt=lt.hasOwnProperty,Zt=lt.toString,Lt=q?q.toStringTag:void 0;var pe=Object.prototype.toString;var _t=q?q.toStringTag:void 0;const De=function xt(_){return null==_?void 0===_?"[object Undefined]":"[object Null]":_t&&_t in Object(_)?function qe(_){var w=zt.call(_,Lt),D=_[Lt];try{_[Lt]=void 0;var I=!0}catch{}var Q=Zt.call(_);return I&&(w?_[Lt]=D:delete _[Lt]),Q}(_):function ho(_){return pe.call(_)}(_)},yt=function uo(_){var w=typeof _;return null!=_&&("object"==w||"function"==w)},me=function Ae(_){if(!yt(_))return!1;var w=De(_);return"[object Function]"==w||"[object GeneratorFunction]"==w||"[object AsyncFunction]"==w||"[object Proxy]"==w};var _,ti=X["__core-js_shared__"],be=(_=/[^.]+$/.exec(ti&&ti.keys&&ti.keys.IE_PROTO||""))?"Symbol(src)_1."+_:"";var Ei=Function.prototype.toString;var Ri=/^\[object .+?Constructor\]$/,ft=RegExp("^"+Function.prototype.toString.call(Object.prototype.hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");const us=function Et(_){return!(!yt(_)||function ei(_){return!!be&&be in _}(_))&&(me(_)?ft:Ri).test(function Te(_){if(null!=_){try{return Ei.call(_)}catch{}try{return _+""}catch{}}return""}(_))},Ft=function vt(_,w){var D=function ii(_,w){return _?.[w]}(_,w);return us(D)?D:void 0},_e=Ft(X,"Map"),Bt=Ft(Object,"create");var bs=Object.prototype.hasOwnProperty;var Ee=Object.prototype.hasOwnProperty;function jt(_){var w=-1,D=null==_?0:_.length;for(this.clear();++w-1&&_%1==0&&_<=9007199254740991},Ui=function En(_){return null!=_&&le(_.length)&&!me(_)};var Zi="object"==typeof exports&&exports&&!exports.nodeType&&exports,pi=Zi&&"object"==typeof module&&module&&!module.nodeType&&module,mi=pi&&pi.exports===Zi?X.Buffer:void 0;const Xi=(mi?mi.isBuffer:void 0)||function In(){return!1};var Kt=Function.prototype.toString,Ki=Object.prototype.hasOwnProperty,Hn=Kt.call(Object);var rt={};rt["[object Float32Array]"]=rt["[object Float64Array]"]=rt["[object Int8Array]"]=rt["[object Int16Array]"]=rt["[object Int32Array]"]=rt["[object Uint8Array]"]=rt["[object Uint8ClampedArray]"]=rt["[object Uint16Array]"]=rt["[object Uint32Array]"]=!0,rt["[object Arguments]"]=rt["[object Array]"]=rt["[object ArrayBuffer]"]=rt["[object Boolean]"]=rt["[object DataView]"]=rt["[object Date]"]=rt["[object Error]"]=rt["[object Function]"]=rt["[object Map]"]=rt["[object Number]"]=rt["[object Object]"]=rt["[object RegExp]"]=rt["[object Set]"]=rt["[object String]"]=rt["[object WeakMap]"]=!1;var Si="object"==typeof exports&&exports&&!exports.nodeType&&exports,ke=Si&&"object"==typeof module&&module&&!module.nodeType&&module,tn=ke&&ke.exports===Si&&Ai.process,Js=function(){try{return ke&&ke.require&&ke.require("util").types||tn&&tn.binding&&tn.binding("util")}catch{}}(),Un=Js&&Js.isTypedArray;const Xn=Un?function qs(_){return function(w){return _(w)}}(Un):function Qs(_){return Be(_)&&le(_.length)&&!!rt[De(_)]},en=function Kn(_,w){if(("constructor"!==w||"function"!=typeof _[w])&&"__proto__"!=w)return _[w]};var h=Object.prototype.hasOwnProperty;const p=function f(_,w,D){var I=_[w];(!h.call(_,w)||!Ut(I,D)||void 0===D&&!(w in _))&&Ni(_,w,D)};var T=/^(?:0|[1-9]\d*)$/;const $=function F(_,w){var D=typeof _;return!!(w=w??9007199254740991)&&("number"==D||"symbol"!=D&&T.test(_))&&_>-1&&_%1==0&&_0){if(++w>=800)return arguments[0]}else w=0;return _.apply(void 0,arguments)}}(Bo);const So=kt,Vo=function No(_,w){return So(function Ro(_,w,D){return w=_o(void 0===w?_.length-1:w,0),function(){for(var I=arguments,Q=-1,st=_o(I.length-w,0),it=Array(st);++Q1?D[Q-1]:void 0,it=Q>2?D[2]:void 0;for(st=_.length>3&&"function"==typeof st?(Q--,st):void 0,it&&function Wo(_,w,D){if(!yt(D))return!1;var I=typeof w;return!!("number"==I?Ui(D)&&$(w,D.length):"string"==I&&w in D)&&Ut(D[w],_)}(D[0],D[1],it)&&(st=Q<3?void 0:st,Q=1),w=Object(w);++I{var _;class w{constructor(){this.colorschemesOptions=new Ot.X(void 0)}setColorschemesOptions(I){this.pColorschemesOptions=I,this.colorschemesOptions.next(I)}getColorschemesOptions(){return this.pColorschemesOptions}}return(_=w).\u0275fac=function(I){return new(I||_)},_.\u0275prov=R.Yz7({token:_,factory:_.\u0275fac,providedIn:"root"}),w})(),es=(()=>{var _;class w{constructor(I,Q,st){this.zone=Q,this.themeService=st,this.type="bar",this.plugins=[],this.chartClick=new R.vpe,this.chartHover=new R.vpe,this.subs=[],this.themeOverrides={},this.ctx=I.nativeElement.getContext("2d"),this.subs.push(this.themeService.colorschemesOptions.pipe((0,St.x)()).subscribe(it=>this.themeChanged(it)))}ngOnChanges(I){const Q=["type"],st=Object.getOwnPropertyNames(I);if(st.some(it=>Q.includes(it))||st.every(it=>I[it].isFirstChange()))this.render();else{const it=this.getChartConfiguration();this.chart&&(Object.assign(this.chart.config.data,it.data),this.chart.config.plugins&&Object.assign(this.chart.config.plugins,it.plugins),this.chart.config.options&&Object.assign(this.chart.config.options,it.options)),this.update()}}ngOnDestroy(){this.chart&&(this.chart.destroy(),this.chart=void 0),this.subs.forEach(I=>I.unsubscribe())}render(){return this.chart&&this.chart.destroy(),this.zone.runOutsideAngular(()=>this.chart=new u.kL(this.ctx,this.getChartConfiguration()))}update(I){this.chart&&this.zone.runOutsideAngular(()=>this.chart?.update(I))}hideDataset(I,Q){this.chart&&(this.chart.getDatasetMeta(I).hidden=Q,this.update())}isDatasetHidden(I){return this.chart?.getDatasetMeta(I)?.hidden}toBase64Image(){return this.chart?.toBase64Image()}themeChanged(I){this.themeOverrides=I,this.chart&&(this.chart.config.options&&Object.assign(this.chart.config.options,this.getChartOptions()),this.update())}getChartOptions(){return wo({onHover:(I,Q)=>{!this.chartHover.observed&&!this.chartHover.observers?.length||this.zone.run(()=>this.chartHover.emit({event:I,active:Q}))},onClick:(I,Q)=>{!this.chartClick.observed&&!this.chartClick.observers?.length||this.zone.run(()=>this.chartClick.emit({event:I,active:Q}))}},this.themeOverrides,this.options,{plugins:{legend:{display:this.legend}}})}getChartConfiguration(){return{type:this.type,data:this.getChartData(),options:this.getChartOptions(),plugins:this.plugins}}getChartData(){return this.data?this.data:{labels:this.labels||[],datasets:this.datasets||[]}}}return(_=w).\u0275fac=function(I){return new(I||_)(R.Y36(R.SBq),R.Y36(R.R0b),R.Y36(ko))},_.\u0275dir=R.lG2({type:_,selectors:[["canvas","baseChart",""]],inputs:{type:"type",legend:"legend",data:"data",options:"options",plugins:"plugins",labels:"labels",datasets:"datasets"},outputs:{chartClick:"chartClick",chartHover:"chartHover"},exportAs:["base-chart"],features:[R.TTD]}),w})();const io=[[255,99,132],[54,162,235],[255,206,86],[231,233,237],[75,192,192],[151,187,205],[220,220,220],[247,70,74],[70,191,189],[253,180,92],[148,159,177],[77,83,96]],sn={plugins:{colors:{enabled:!1}},datasets:{line:{backgroundColor:_=>Pt(Ce(_.datasetIndex),.4),borderColor:_=>Pt(Ce(_.datasetIndex),1),pointBackgroundColor:_=>Pt(Ce(_.datasetIndex),1),pointBorderColor:"#fff"},bar:{backgroundColor:_=>Pt(Ce(_.datasetIndex),.6),borderColor:_=>Pt(Ce(_.datasetIndex),1)},get radar(){return this.line},doughnut:{backgroundColor:_=>Pt(Ce(_.dataIndex),.6),borderColor:"#fff"},get pie(){return this.doughnut},polarArea:{backgroundColor:_=>Pt(Ce(_.dataIndex),.6),borderColor:_=>Pt(Ce(_.dataIndex),1)},get bubble(){return this.doughnut},get scatter(){return this.doughnut},get area(){return this.polarArea}}};function Pt(_,w){return"rgba("+_.concat(w).join(",")+")"}function is(_,w){return Math.floor(Math.random()*(w-_+1))+_}function Ce(_=0){return io[_]||function Po(){return[is(0,255),is(0,255),is(0,255)]}()}let Co=(()=>{var _;class w{constructor(){this.generateColors=!0}}return(_=w).\u0275fac=function(I){return new(I||_)},_.\u0275prov=R.Yz7({token:_,factory:_.\u0275fac,providedIn:"root"}),w})();u.kL.register(...u.zX);let no=(()=>{var _;class w{constructor(I){I?.plugins&&u.kL.register(...I.plugins);const Q=wo(I?.generateColors?sn:{},I?.defaults||{});E.d.set(Q)}static forRoot(I){return{ngModule:w,providers:[{provide:Co,useValue:I}]}}}return(_=w).\u0275fac=function(I){return new(I||_)(R.LFG(Co,8))},_.\u0275mod=R.oAB({type:_}),_.\u0275inj=R.cJS({}),w})()}}]); \ No newline at end of file diff --git a/worklenz-backend/src/public/150.aea42238d373936d.js b/worklenz-backend/src/public/150.aea42238d373936d.js new file mode 100644 index 00000000..d1de3689 --- /dev/null +++ b/worklenz-backend/src/public/150.aea42238d373936d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkworklenz=self.webpackChunkworklenz||[]).push([[150],{39787:(ye,se,c)=>{c.d(se,{l:()=>Y});var h=c(96109),N=c(42840),e=c(62595),w=c(65879),U=c(72042),D=c(70855);let Y=(()=>{var v;class T{constructor(X){this.menu=X}}return(v=T).\u0275fac=function(X){return new(X||v)(w.Y36(U.h))},v.\u0275cmp=w.Xpm({type:v,selectors:[["worklenz-toggle-menu-button"]],inputs:{key:"key"},standalone:!0,features:[w.jDz],decls:2,vars:2,consts:[["nz-button","","nz-tooltip","","nzShape","circle","nzType","link",3,"nzTooltipTitle","click"],["nz-icon","","nzType","pushpin",1,"pin-button",3,"nzTheme"]],template:function(X,ee){1&X&&(w.TgZ(0,"button",0),w.NdJ("click",function(){return ee.menu.toggle(ee.key)}),w._UZ(1,"span",1),w.qZA()),2&X&&(w.Q6J("nzTooltipTitle",ee.menu.isPinned(ee.key)?"Click to unpin this from the main menu":"Click to pin this into the main menu"),w.xp6(1),w.Q6J("nzTheme",ee.menu.isPinned(ee.key)?"fill":"outline"))},dependencies:[h.cg,h.SY,N.sL,N.ix,D.w,e.PV,e.Ls]}),T})()},16150:(ye,se,c)=>{c.r(se),c.d(se,{SettingsModule:()=>go});var h=c(96814),N=c(35420),e=c(65879),w=c(32333),U=c(90586),D=c(33410),Y=c(26254),v=c(70855),T=c(73460);const re=function(i){return[i]},X=function(){return["ant-menu-item-selected"]};function ee(i,a){if(1&i&&e._UZ(0,"li",7),2&i){const s=a.$implicit;e.Q6J("routerLink",e.VKq(4,re,s.url))("nzIcon",s.icon)("nzTitle",s.label)("routerLinkActive",e.DdM(6,X))}}let ve=(()=>{var i;class a{constructor(t,n){this.auth=t,this.app=n,this.navigation=[],this.app.setTitle("Settings")}get profile(){return this.auth.getCurrentSession()}ngOnInit(){this.buildNavigation()}isOwnerOrAdmin(){return this.profile?.owner||this.profile?.is_admin}buildNavigation(){this.navigation=[],this.navigation.push({label:"Profile",icon:"user",url:"profile"}),this.navigation.push({label:"Notifications",icon:"notification",url:"notifications"}),this.profile?.is_expired||(this.isOwnerOrAdmin()&&(this.navigation.push({label:"Clients",icon:"user-switch",url:"clients"}),this.navigation.push({label:"Job Titles",icon:"idcard",url:"job-titles"}),this.navigation.push({label:"Labels",icon:"tags",url:"labels"}),this.navigation.push({label:"Categories",icon:"group",url:"categories"}),this.navigation.push({label:"Project Templates",icon:"file-zip",url:"project-templates"}),this.navigation.push({label:"Task Templates",icon:"profile",url:"task-templates"}),this.navigation.push({label:"Team Members",icon:"team",url:"team-members"})),this.navigation.push({label:"Teams",icon:"bank",url:"teams"})),this.profile?.is_google||this.navigation.push({label:"Change Password",icon:"lock",url:"password"}),this.navigation.push({label:"Language & Region",icon:"global",url:"language-and-region"})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(w.e),e.Y36(U.z))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-settings"]],decls:10,vars:4,consts:[[1,"container"],[1,"px-0",3,"nzGhost"],[1,"inner-layout"],[3,"nzWidth"],["nz-menu","",1,"border-0",3,"nzMode"],["class","rounded-4","nz-submenu","",3,"routerLink","nzIcon","nzTitle","routerLinkActive",4,"ngFor","ngForOf"],[1,"px-4","bg-white"],["nz-submenu","",1,"rounded-4",3,"routerLink","nzIcon","nzTitle","routerLinkActive"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"nz-page-header",1)(2,"nz-page-header-title"),e._uU(3,"Settings"),e.qZA()(),e.TgZ(4,"nz-layout",2)(5,"nz-sider",3)(6,"ul",4),e.YNc(7,ee,1,7,"li",5),e.qZA()(),e.TgZ(8,"nz-content",6),e._UZ(9,"router-outlet"),e.qZA()()()),2&t&&(e.xp6(1),e.Q6J("nzGhost",!1),e.xp6(4),e.Q6J("nzWidth","240px"),e.xp6(1),e.Q6J("nzMode","vertical"),e.xp6(1),e.Q6J("ngForOf",n.navigation))},dependencies:[h.sg,N.lC,N.rH,N.Od,D.hw,D.OK,D.t7,Y.$O,Y.u9,v.w,T.wO,T.rY],styles:["[nz-submenu][_ngcontent-%COMP%]{transition:none!important}"]}),a})();var m=c(15861),p=c(60095),me=c(59556),b=c(69649),A=c(86408),I=c(27782),he=c(16849),Se=c(44568),V=c(82962),M=c(62595),Z=c(10095),x=c(3599),P=c(20824),O=c(42840),L=c(41958),Q=c(8083),J=c(96109),$=c(24139),ge=c(34302);function Ue(i,a){1&i&&e._UZ(0,"span",22)}function Pe(i,a){1&i&&e._UZ(0,"span",23)}function u(i,a){if(1&i&&(e.TgZ(0,"div",18),e.YNc(1,Ue,1,0,"span",19),e.YNc(2,Pe,1,0,"span",20),e.TgZ(3,"div",21),e._uU(4),e.qZA()()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngIf",!s.uploading),e.xp6(1),e.Q6J("ngIf",s.uploading),e.xp6(2),e.Oqu(s.uploading?"Uploading...":"Upload")}}function g(i,a){if(1&i&&e._UZ(0,"img",24),2&i){const s=e.oxw();e.Q6J("alt",null==s.profile?null:s.profile.name)("src",s.avatarUrl,e.LSH)}}function C(i,a){if(1&i&&(e.TgZ(0,"p",25)(1,"small",26),e.ALo(2,"date"),e._uU(3),e.ALo(4,"fromNow"),e.qZA()()),2&i){const s=e.oxw();e.xp6(1),e.s9C("nzTooltipTitle",e.xi3(2,2,null==s.profile?null:s.profile.joined_date,"medium")),e.xp6(2),e.hij(" Joined ",e.lcZ(4,5,null==s.profile?null:s.profile.joined_date)," ")}}function f(i,a){if(1&i&&(e.TgZ(0,"p",25)(1,"small",26),e.ALo(2,"date"),e._uU(3),e.ALo(4,"fromNow"),e.qZA()()),2&i){const s=e.oxw();e.xp6(1),e.s9C("nzTooltipTitle",e.xi3(2,2,null==s.profile?null:s.profile.last_updated,"medium")),e.xp6(2),e.hij(" Last updated ",e.lcZ(4,5,null==s.profile?null:s.profile.last_updated)," ")}}let F=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.fb=t,this.api=n,this.app=o,this.auth=r,this.attachmentsApi=l,this.ref=d,this.model={},this.loading=!1,this.updating=!1,this.uploading=!1,this.avatarUrl=null,this.app.setTitle("Profile Settings"),A.s.track(I.S5),this.form=this.fb.group({name:[null,p.kI.required],email:[null,p.kI.required]}),this.avatarUrl=this.profile?.avatar_url||null,this.form.controls.email.disable()}get profile(){return this.auth.getCurrentSession()}ngOnInit(){var t=this;return(0,m.Z)(function*(){yield t.get()})()}isInvalidForm(){return this.form.invalid}get(){var t=this;return(0,m.Z)(function*(){t.loading=!0;try{const n=yield t.api.get();n.done&&(t.model=n.body,t.form.controls.name.setValue(t.model.name),t.form.controls.email.setValue(t.model.email),t.profile?.is_google&&t.form.controls.email.disable()),t.loading=!1}catch(n){t.loading=!1,(0,b.tu)(n)}})()}submit(){var t=this;return(0,m.Z)(function*(){if(t.form.invalid)t.app.displayErrorsOf(t.form);else{t.updating=!0;try{const n={name:t.form.controls.name.value};(yield t.api.update(n)).done&&(A.s.track(I.AE),yield t.get(),yield t.auth.authorize(),(0,me.Bk)()),t.updating=!1}catch(n){t.updating=!1,(0,b.tu)(n)}}})()}uploadFile(t){var n=this;return(0,m.Z)(function*(){if(n.uploading)return;try{const r=t.files||[];if(!r||!r.length)return;const l=r[0];n.uploading=!0;const d=yield(0,b.y3)(l),_=yield n.attachmentsApi.createAvatarAttachment({file:d,file_name:l.name,size:l.size});_.done&&(A.s.track(I.nX),yield n.auth.authorize(),(0,me.hQ)(),n.avatarUrl=_.body.url),n.uploading=!1}catch{n.uploading=!1}const o=new DataTransfer;t.files=o.files})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(p.qu),e.Y36(he.G),e.Y36(U.z),e.Y36(w.e),e.Y36(Se.J),e.Y36(e.z2F))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-profile"]],decls:26,vars:16,consts:[[3,"nzActive","nzLoading"],[1,"ant-upload-list","ant-upload-list-picture-card","mb-3"],[1,"ant-upload","ant-upload-select","ant-upload-select-picture-card",2,"position","relative"],["tabindex","0","role","button","nz-tooltip","",1,"ant-upload",3,"nzTooltipTitle","click"],["type","file","accept","image/png, image/jpg, image/jpeg",2,"display","none",3,"change"],["fileInput",""],["class","avatar-upload-icon",4,"ngIf"],["style","width: 100%",3,"alt","src",4,"ngIf"],["nz-form","","nzLayout","vertical",3,"formGroup","ngSubmit"],["nzFor","name","nzRequired",""],["nzErrorTip","Name is required!",3,"nzSm","nzXs"],["id","name","nz-input","",3,"formControlName"],["nzFor","email","nzRequired",""],["nzErrorTip","The input is not valid E-mail!",3,"nzSm","nzXs"],["id","email","nz-input","","readonly","",3,"formControlName"],["nz-row","",1,"register-area","mb-3"],["nz-button","","nzType","primary",3,"disabled","nzLoading"],["class","mb-0","nz-typography","","nzType","secondary",4,"ngIf"],[1,"avatar-upload-icon"],["nz-icon","","nzType","plus","nzTheme","outline",4,"ngIf"],["nz-icon","","nzType","loading","nzTheme","outline",4,"ngIf"],[2,"margin-top","8px"],["nz-icon","","nzType","plus","nzTheme","outline"],["nz-icon","","nzType","loading","nzTheme","outline"],[2,"width","100%",3,"alt","src"],["nz-typography","","nzType","secondary",1,"mb-0"],["nz-tooltip","","nzTooltipPlacement","right",3,"nzTooltipTitle"]],template:function(t,n){if(1&t){const o=e.EpF();e.TgZ(0,"nz-card")(1,"nz-skeleton",0)(2,"div",1)(3,"div",2)(4,"div",3),e.NdJ("click",function(){e.CHM(o);const l=e.MAs(6);return e.KtG(l.click())}),e.TgZ(5,"input",4,5),e.NdJ("change",function(){e.CHM(o);const l=e.MAs(6);return e.KtG(n.uploadFile(l))}),e.qZA(),e.YNc(7,u,5,3,"div",6),e.YNc(8,g,1,2,"img",7),e.qZA()()(),e.TgZ(9,"form",8),e.NdJ("ngSubmit",function(){return n.submit()}),e.TgZ(10,"nz-form-item")(11,"nz-form-label",9),e._uU(12,"Name"),e.qZA(),e.TgZ(13,"nz-form-control",10),e._UZ(14,"input",11),e.qZA()(),e.TgZ(15,"nz-form-item")(16,"nz-form-label",12),e._uU(17,"E-mail"),e.qZA(),e.TgZ(18,"nz-form-control",13),e._UZ(19,"input",14),e.qZA()(),e.TgZ(20,"nz-form-item",15)(21,"nz-form-control")(22,"button",16),e._uU(23," Save Changes "),e.qZA()()(),e.YNc(24,C,5,7,"p",17),e.YNc(25,f,5,7,"p",17),e.qZA()()()}2&t&&(e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loading||n.updating),e.xp6(3),e.Q6J("nzTooltipTitle",n.avatarUrl?"Click to change the avatar":"Click to upload an avatar"),e.xp6(3),e.Q6J("ngIf",!n.avatarUrl||n.uploading),e.xp6(1),e.Q6J("ngIf",n.avatarUrl),e.xp6(1),e.Q6J("formGroup",n.form),e.xp6(4),e.Q6J("nzSm",14)("nzXs",24),e.xp6(1),e.Q6J("formControlName","name"),e.xp6(4),e.Q6J("nzSm",14)("nzXs",24),e.xp6(1),e.Q6J("formControlName","email"),e.xp6(3),e.Q6J("disabled",n.isInvalidForm()||n.loading)("nzLoading",n.updating),e.xp6(2),e.Q6J("ngIf",null==n.profile?null:n.profile.joined_date),e.xp6(1),e.Q6J("ngIf",null==n.profile?null:n.profile.last_updated))},dependencies:[h.O5,V.bd,M.Ls,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,P.Zp,O.ix,v.w,L.dQ,Q.ZU,J.SY,$.ng,h.uU,ge.d],styles:["[nz-form][_ngcontent-%COMP%]{max-width:600px}.avatar-upload-icon[_ngcontent-%COMP%]{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.6196078431)}"]}),a})();var pe=c(83916),te=c(72042),z=c(13740),fe=c(81221),H=c(92574),Me=c(39787),ie=c(33640);function Ke(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",19),e.NdJ("click",function(){e.CHM(s);const n=e.oxw(2);return e.KtG(n.editTeam(n.currentTeam))}),e._UZ(1,"span",20),e.qZA()}}function We(i,a){if(1&i&&(e.TgZ(0,"tr",14)(1,"td"),e._UZ(2,"nz-badge",15),e.qZA(),e.TgZ(3,"td"),e._uU(4),e.ALo(5,"fromNow"),e.qZA(),e.TgZ(6,"td"),e._uU(7),e.qZA(),e.TgZ(8,"td",16)(9,"div",17)(10,"nz-space"),e.YNc(11,Ke,2,0,"button",18),e.qZA()()()()),2&i){const s=e.oxw();e.xp6(2),e.Q6J("nzText",s.currentTeam.name),e.xp6(2),e.Oqu(e.lcZ(5,4,s.currentTeam.created_at)),e.xp6(3),e.Oqu(s.currentTeam.owns_by),e.xp6(4),e.Q6J("ngIf",s.currentTeam.owner)}}function Xe(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",19),e.NdJ("click",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw();return e.KtG(o.editTeam(n))}),e._UZ(1,"span",20),e.qZA()}}function et(i,a){if(1&i&&(e.TgZ(0,"tr",14)(1,"td"),e._uU(2),e.qZA(),e.TgZ(3,"td"),e._uU(4),e.ALo(5,"fromNow"),e.qZA(),e.TgZ(6,"td"),e._uU(7),e.qZA(),e.TgZ(8,"td",16)(9,"div",17)(10,"nz-space"),e.YNc(11,Xe,2,0,"button",18),e.qZA()()()()),2&i){const s=a.$implicit;e.xp6(2),e.Oqu(s.name),e.xp6(2),e.Oqu(e.lcZ(5,4,s.created_at)),e.xp6(3),e.Oqu(s.owns_by),e.xp6(4),e.Q6J("ngIf",s.owner)}}function tt(i,a){if(1&i&&e._UZ(0,"worklenz-toggle-menu-button",1),2&i){const s=e.oxw();e.Q6J("key",s.menu.TEAMS_MENU)}}function nt(i,a){1&i&&(e.TgZ(0,"div",21)(1,"div",22),e._UZ(2,"img",23),e.qZA(),e.TgZ(3,"span",24),e._uU(4,"No teams found."),e.qZA()())}function it(i,a){}function st(i,a){if(1&i&&(e.ynx(0),e.TgZ(1,"form",25)(2,"nz-form-item")(3,"nz-form-label",26),e._uU(4,"Name"),e.qZA(),e.TgZ(5,"nz-form-control",27),e._UZ(6,"input",28),e.qZA()()(),e.BQk()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("formGroup",s.form)("nzLayout","vertical"),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(1),e.Q6J("formControlName","name")}}const ot=function(){return{rows:5}};let at=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.teamsApi=t,this.settingsApi=n,this.fb=o,this.app=r,this.menu=l,this.auth=d,this.teams=[],this.currentTeam=null,this.loading=!1,this.updating=!1,this.showTeamEditModal=!1,this.editingTeamId=null,this.app.setTitle("Teams"),A.s.track(I.jD),this.form=this.fb.group({name:[null,p.kI.required]})}ngOnInit(){this.getTeams()}getTeams(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.teamsApi.get();n.done&&(t.teams=n.body.filter(o=>o.id!==t.auth.getCurrentSession()?.team_id),t.currentTeam=n.body.filter(o=>o.id===t.auth.getCurrentSession()?.team_id)[0]),t.loading=!1}catch(n){t.loading=!1,(0,b.tu)(n)}})()}updateTeamName(t,n){var o=this;return(0,m.Z)(function*(){try{if(!t||!n)return;o.updating=!0,(yield o.settingsApi.updateTeamName(t,{name:n})).done?window.location.reload():o.updating=!1}catch(r){o.updating=!1,(0,b.tu)(r)}})()}closeModal(){this.showTeamEditModal=!1,this.editingTeamId=null,this.form.reset()}handleOk(){var t=this;return(0,m.Z)(function*(){t.form.valid&&t.editingTeamId&&(yield t.updateTeamName(t.editingTeamId,t.form.controls.name.value))})()}editTeam(t){!t||!t.id||(this.showTeamEditModal=!0,this.editingTeamId=t.id,this.form.controls.name.setValue(t.name))}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(pe.S),e.Y36(he.G),e.Y36(p.qu),e.Y36(U.z),e.Y36(te.h),e.Y36(w.e))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-teams"]],decls:28,vars:14,consts:[[1,"px-0",3,"nzGhost"],[3,"key"],[3,"nzActive","nzLoading","nzParagraph"],["nzPaginationType","small","nzSize","small",1,"custom-table",3,"nzData","nzLoading","nzNoResult"],["table",""],["scope","col"],["colspan","2","nzAlign","left","scope","col"],["class","actions-row",4,"ngIf"],["class","actions-row",4,"ngFor","ngForOf"],["actionsTemplate",""],["noDataTemplate",""],["noDataTemplate1",""],["nzOkText","Update Name","nzTitle","Edit Team Name",3,"nzVisible","nzOkLoading","nzOnCancel","nzOnOk","nzVisibleChange"],[4,"nzModalContent"],[1,"actions-row"],["nzColor","#52c41a",3,"nzText"],["nzAlign","center",1,"actions-col"],[1,"actions"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click",4,"ngIf"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click"],["nz-icon","","nzType","edit"],[1,"pt-4","pb-5"],[1,"no-data-img-holder","mx-auto","mb-3"],["src","/assets/images/empty-box.webp","alt","",1,"img-fluid"],["nz-typography","",1,"no-data-text"],["nz-form","",3,"formGroup","nzLayout"],["nzRequired","",3,"nzSpan"],["nzErrorTip","Please enter a name!",3,"nzSpan"],["nz-input","","placeholder","Name",3,"formControlName"]],template:function(t,n){if(1&t&&(e.TgZ(0,"nz-page-header",0)(1,"nz-page-header-title"),e._uU(2),e.qZA(),e.TgZ(3,"nz-page-header-extra"),e._UZ(4,"worklenz-toggle-menu-button",1),e.qZA()(),e.TgZ(5,"nz-card")(6,"nz-skeleton",2)(7,"nz-table",3,4)(9,"thead")(10,"tr")(11,"th",5),e._uU(12,"Name"),e.qZA(),e.TgZ(13,"th",5),e._uU(14,"Created"),e.qZA(),e.TgZ(15,"th",6),e._uU(16,"Owns By"),e.qZA()()(),e.TgZ(17,"tbody"),e.YNc(18,We,12,6,"tr",7),e.YNc(19,et,12,6,"tr",8),e.qZA()()()(),e.YNc(20,tt,1,1,"ng-template",null,9,e.W1O),e.YNc(22,nt,5,0,"ng-template",null,10,e.W1O),e.YNc(24,it,0,0,"ng-template",null,11,e.W1O),e.TgZ(26,"nz-modal",12),e.NdJ("nzOnCancel",function(){return n.closeModal()})("nzOnOk",function(){return n.handleOk()})("nzVisibleChange",function(r){return n.showTeamEditModal=r}),e.YNc(27,st,7,5,"ng-container",13),e.qZA()),2&t){const o=e.MAs(8),r=e.MAs(23),l=e.MAs(25);e.Q6J("nzGhost",!1),e.xp6(2),e.hij("",n.teams.length?n.teams.length+1:0," Teams"),e.xp6(2),e.Q6J("key",n.menu.TEAMS_MENU),e.xp6(2),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(13,ot)),e.xp6(1),e.Q6J("nzData",n.teams)("nzLoading",n.loading)("nzNoResult",n.currentTeam?l:r),e.xp6(11),e.Q6J("ngIf",n.currentTeam),e.xp6(1),e.Q6J("ngForOf",o.data),e.xp6(7),e.Q6J("nzVisible",n.showTeamEditModal)("nzOkLoading",n.updating)}},dependencies:[h.sg,h.O5,V.bd,Y.$O,Y.u9,Y.Jp,M.Ls,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,P.Zp,O.ix,v.w,L.dQ,z.N8,z.Uo,z._C,z.Om,z.p0,z.$Z,z.UX,fe.du,fe.Hf,Q.ZU,H.NU,J.SY,$.ng,Me.l,ie.x7,ge.d],styles:[".no-data-img-holder[_ngcontent-%COMP%]{width:100px;margin-top:42px}"]}),a})();var j=c(21406),le=c(26857),ce=c(69862);let rt=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t}changePassword(t){return this._post(this.http,`${this.API_BASE_URL}/change-password`,t)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})();function lt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"span",15),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.oldPasswordVisible=!n.oldPasswordVisible)}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("nzType",s.oldPasswordVisible?"eye-invisible":"eye")}}function ct(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"span",15),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.newPasswordVisible=!n.newPasswordVisible)}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("nzType",s.newPasswordVisible?"eye-invisible":"eye")}}function pt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"span",15),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.confirmPasswordVisible=!n.confirmPasswordVisible)}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("nzType",s.confirmPasswordVisible?"eye-invisible":"eye")}}let dt=(()=>{var i;class a{constructor(t,n,o){this.usersService=t,this.fb=n,this.app=o,this.oldPasswordVisible=!1,this.newPasswordVisible=!1,this.confirmPasswordVisible=!1,this.showTaskModal=!1,this.loading=!1,this.passwordPolicy=j.QW,this.app.setTitle("Change Password"),A.s.track(I.yv),this.passwordChangeForm=this.fb.group({password:[null,[p.kI.required]],new_password:[null,[p.kI.required]],confirm_password:[null,[p.kI.required]]},{validators:[this.confirmPasswordsValidator]})}confirmPasswordsValidator(t){return(0,m.Z)(function*(){return t.controls.confirm_password.value!==t.controls.new_password.value?t.controls.confirm_password.setErrors({passwordMismatch:!0}):t.controls.confirm_password})()}submitForm(){var t=this;return(0,m.Z)(function*(){const n={password:t.passwordChangeForm.value.password,new_password:t.passwordChangeForm.value.new_password,confirm_password:t.passwordChangeForm.value.confirm_password};n.password&&n.new_password&&n.confirm_password&&(t.passwordChangeForm.setErrors({invalid:!1}),(yield t.usersService.changePassword(n)).done&&(t.passwordChangeForm.reset(),t.showTaskModal=!1))})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(rt),e.Y36(p.qu),e.Y36(U.z))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-change-password"]],decls:35,vars:26,consts:[["nz-form","",3,"formGroup","nzLayout","submit"],[3,"nzSm","nzXs","nzErrorTip"],["nzRequired",""],[3,"nzSuffix"],["autocomplete","off","nz-input","","placeholder","Enter your current password",3,"type","formControlName"],["suffixTemplateOld",""],["autocomplete","off","nz-input","","placeholder","New Password",3,"type","formControlName"],["suffixTemplateNew",""],[1,"mb-2"],[3,"nzErrorTip","nzSm","nzXs"],["autocomplete","off","nz-input","","placeholder","Confirm New Password",3,"type","formControlName"],["suffixTemplateConfirm",""],[3,"nzSm","nzXs"],["nz-typography","",1,"d-block",2,"font-size","12px",3,"nzType"],["nz-button","","nzType","primary","type","submit"],["nz-icon","",3,"nzType","click"]],template:function(t,n){if(1&t&&(e.TgZ(0,"nz-card")(1,"form",0),e.NdJ("submit",function(){return n.submitForm()}),e.TgZ(2,"nz-form-item")(3,"nz-form-control",1)(4,"nz-form-label",2),e._uU(5,"Current Password"),e.qZA(),e.TgZ(6,"nz-input-group",3),e._UZ(7,"input",4),e.qZA(),e.YNc(8,lt,1,1,"ng-template",null,5,e.W1O),e.qZA()(),e.TgZ(10,"nz-form-item")(11,"nz-form-control",1)(12,"nz-form-label",2),e._uU(13,"New Password"),e.qZA(),e.TgZ(14,"nz-input-group",3),e._UZ(15,"input",6),e.qZA(),e.YNc(16,ct,1,1,"ng-template",null,7,e.W1O),e.qZA()(),e.TgZ(18,"nz-form-item",8)(19,"nz-form-control",9)(20,"nz-form-label",2),e._uU(21,"Confirm New Password"),e.qZA(),e.TgZ(22,"nz-input-group",3),e._UZ(23,"input",10),e.qZA(),e.YNc(24,pt,1,1,"ng-template",null,11,e.W1O),e.qZA()(),e.TgZ(26,"nz-form-item",8)(27,"nz-form-control",12)(28,"span",13),e._uU(29),e.ALo(30,"lowercase"),e.qZA()()(),e.TgZ(31,"nz-form-item")(32,"nz-form-control")(33,"button",14),e._uU(34," Update "),e.qZA()()()()()),2&t){const o=e.MAs(9),r=e.MAs(17),l=e.MAs(25);let d;e.xp6(1),e.Q6J("formGroup",n.passwordChangeForm)("nzLayout","vertical"),e.xp6(2),e.Q6J("nzSm",14)("nzXs",24)("nzErrorTip","Please input your old password!"),e.xp6(3),e.Q6J("nzSuffix",o),e.xp6(1),e.Q6J("type",n.oldPasswordVisible?"text":"password")("formControlName","password"),e.xp6(4),e.Q6J("nzSm",14)("nzXs",24)("nzErrorTip","Please input your new password!"),e.xp6(3),e.Q6J("nzSuffix",r),e.xp6(1),e.Q6J("type",n.newPasswordVisible?"text":"password")("formControlName","new_password"),e.xp6(4),e.Q6J("nzErrorTip",null!=(d=n.passwordChangeForm.get("confirm_password"))&&d.hasError("passwordMismatch")?"Passwords do not match!":"Please confirm your new password!")("nzSm",14)("nzXs",24),e.xp6(3),e.Q6J("nzSuffix",l),e.xp6(1),e.Q6J("type",n.confirmPasswordVisible?"text":"password")("formControlName","confirm_password"),e.xp6(4),e.Q6J("nzSm",14)("nzXs",24),e.xp6(1),e.Q6J("nzType","secondary"),e.xp6(1),e.hij("New password should be ",e.lcZ(30,24,n.passwordPolicy),"")}},dependencies:[V.bd,M.Ls,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,P.Zp,P.gB,P.ke,O.ix,v.w,L.dQ,Q.ZU,h.i8],styles:["[nz-form][_ngcontent-%COMP%]{max-width:600px}"]}),a})();var oe=c(59780);let ut=(()=>{var i;class a{constructor(t,n){this.auth=t,this.router=n}canActivate(t,n){return!this.auth.getCurrentSession()?.is_google||this.router.navigate(["/worklenz"])}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(w.e),e.LFG(N.F0))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),mt=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/timezones`}update(t){return this._put(this.http,this.root,t)}get(){return this._get(this.http,this.root)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})();var K=c(9691);function ht(i,a){if(1&i&&(e.TgZ(0,"nz-option",9),e._uU(1),e.qZA()),2&i){const s=a.$implicit;e.Q6J("nzLabel",s)("nzValue",s),e.xp6(1),e.hij(" ",s," ")}}function gt(i,a){if(1&i&&(e.TgZ(0,"nz-option",9)(1,"div",10)(2,"span"),e._uU(3),e.qZA(),e.TgZ(4,"span",11),e._uU(5),e.qZA()()()),2&i){const s=a.$implicit;e.s9C("nzLabel",s.name),e.Q6J("nzValue",s.id),e.xp6(3),e.Oqu(s.name),e.xp6(2),e.Oqu(s.abbrev)}}let ft=(()=>{var i;class a{constructor(t,n,o,r){this.app=t,this.api=n,this.fb=o,this.auth=r,this.loading=!1,this.updating=!1,this.options=["English"],this.timezones=[],this.app.setTitle("Language & Region"),A.s.track(I.xH);const l=this.auth.getCurrentSession();this.form=this.fb.group({language:["English",p.kI.required],timezone:[l?.timezone||null,p.kI.required]})}ngOnInit(){this.get()}submit(){var t=this;return(0,m.Z)(function*(){if(t.form.invalid)return t.app.displayErrorsOf(t.form);try{t.updating=!0,(yield t.api.update(t.form.value)).done&&(yield t.auth.authorize()),t.updating=!1}catch{t.updating=!1}})()}get(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.api.get();n.done&&(t.timezones=n.body),t.loading=!1}catch{t.loading=!1}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(U.z),e.Y36(mt),e.Y36(p.qu),e.Y36(w.e))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-language-and-region"]],decls:19,vars:13,consts:[[3,"nzActive","nzLoading"],["nz-form","","nzLayout","vertical",3,"formGroup","ngSubmit"],["nzFor","name","nzRequired",""],[3,"nzSm","nzXs"],[3,"formControlName"],["nzCustomContent","",3,"nzLabel","nzValue",4,"ngFor","ngForOf"],["nzShowSearch","",3,"formControlName"],["nz-row","",1,"register-area"],["nz-button","","nzType","primary",3,"disabled","nzLoading"],["nzCustomContent","",3,"nzLabel","nzValue"],[1,"d-flex","justify-content-between"],["nz-typography","","nzType","secondary"]],template:function(t,n){1&t&&(e.TgZ(0,"nz-card")(1,"nz-skeleton",0)(2,"form",1),e.NdJ("ngSubmit",function(){return n.submit()}),e.TgZ(3,"nz-form-item")(4,"nz-form-label",2),e._uU(5,"Language"),e.qZA(),e.TgZ(6,"nz-form-control",3)(7,"nz-select",4),e.YNc(8,ht,2,3,"nz-option",5),e.qZA()()(),e.TgZ(9,"nz-form-item")(10,"nz-form-label",2),e._uU(11,"Time zone"),e.qZA(),e.TgZ(12,"nz-form-control",3)(13,"nz-select",6),e.YNc(14,gt,6,4,"nz-option",5),e.qZA()()(),e.TgZ(15,"nz-form-item",7)(16,"nz-form-control")(17,"button",8),e._uU(18," Save Changes "),e.qZA()()()()()()),2&t&&(e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loading||n.updating),e.xp6(1),e.Q6J("formGroup",n.form),e.xp6(4),e.Q6J("nzSm",8)("nzXs",24),e.xp6(1),e.Q6J("formControlName","language"),e.xp6(1),e.Q6J("ngForOf",n.options),e.xp6(4),e.Q6J("nzSm",8)("nzXs",24),e.xp6(1),e.Q6J("formControlName","timezone"),e.xp6(1),e.Q6J("ngForOf",n.timezones),e.xp6(3),e.Q6J("disabled",n.loading)("nzLoading",n.updating))},dependencies:[h.sg,V.bd,p._Y,p.JJ,p.JL,p.sg,p.u,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,O.ix,v.w,L.dQ,Q.ZU,$.ng,K.Ip,K.Vq]}),a})();var zt=c(8725),Oe=c(9172),E=c(62787),ze=c(55695),Te=c(19035);function Tt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",17),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw().$implicit,l=e.oxw();return e.KtG(l.updateColorCode(r.id,o))}),e.TgZ(1,"nz-tag",18),e._uU(2),e.qZA()()}if(2&i){const s=a.$implicit,t=e.oxw().$implicit,n=e.oxw();e.xp6(1),e.Q6J("nzColor",s+n.alpha),e.xp6(1),e.Oqu(t.name)}}function Ct(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"tr",8)(1,"td")(2,"nz-tag",9),e._uU(3),e.qZA(),e.TgZ(4,"nz-dropdown-menu",null,10)(6,"ul",11),e.YNc(7,Tt,3,2,"li",12),e.qZA()()(),e.TgZ(8,"td"),e._uU(9),e.qZA(),e.TgZ(10,"td",13)(11,"div",14)(12,"nz-space")(13,"button",15),e.NdJ("nzOnConfirm",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.deleteLabel(o))}),e._UZ(14,"span",16),e.qZA()()()()()}if(2&i){const s=a.$implicit,t=e.MAs(5),n=e.oxw();e.xp6(2),e.Q6J("nzColor",s.color_code+n.alpha)("nzDropdownMenu",t)("nzTooltipTitle","Click to change color")("nzTrigger","click"),e.xp6(1),e.Oqu(s.name),e.xp6(4),e.Q6J("ngForOf",n.colorCodes),e.xp6(2),e.Oqu(s.usage),e.xp6(4),e.Q6J("nzOkText","Yes")("nzPopconfirmTitle","Are you sure?")("nzSize","small")("nzTooltipPlacement","top")("nzTooltipTitle","Delete")("nzType","default")}}function kt(i,a){1&i&&e._UZ(0,"span",25)}function xt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"form",21)(1,"nz-input-group",22)(2,"input",23),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.labelsSearch=n)})("ngModelChange",function(n){e.CHM(s);const o=e.oxw(2);return e.KtG(o.searchLabels(n))}),e.qZA()(),e.YNc(3,kt,1,0,"ng-template",null,24,e.W1O),e.qZA()}if(2&i){const s=e.MAs(4),t=e.oxw(2);e.Q6J("nzLayout","vertical"),e.xp6(1),e.Q6J("nzSuffix",s),e.xp6(1),e.Q6J("ngModel",t.labelsSearch)}}function bt(i,a){if(1&i&&e._UZ(0,"worklenz-toggle-menu-button",26),2&i){const s=e.oxw(2);e.Q6J("key",s.menu.LABELS_MENU)}}function vt(i,a){1&i&&(e.TgZ(0,"nz-space"),e.YNc(1,xt,5,3,"form",19),e.YNc(2,bt,1,1,"worklenz-toggle-menu-button",20),e.qZA())}const wt=function(){return{rows:5}};let yt=(()=>{var i;class a{constructor(t,n,o,r,l){this.app=t,this.api=n,this.searchPipe=o,this.cdr=r,this.menu=l,this.colorCodes=j.lD,this.labels=[],this.filteredLabels=[],this.loading=!1,this.alpha=j.Yj,this.labelsSearch=null,this.app.setTitle("Manage Labels"),A.s.track(I.Hw)}ngOnInit(){this.get()}trackByFn(t,n){return n.id}get(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.api.get();n.done&&(t.labels=n.body,t.filteredLabels=t.labels),t.loading=!1}catch(n){t.loading=!1,(0,b.tu)(n)}t.cdr.detectChanges()})()}deleteLabel(t){var n=this;return(0,m.Z)(function*(){if(t?.id){try{n.loading=!0,(yield n.api.deleteById(t.id)).done&&(A.s.track(I.aJ),n.get()),n.loading=!1}catch(o){n.loading=!1,(0,b.tu)(o)}n.cdr.detectChanges()}})()}updateColorCode(t,n){var o=this;return(0,m.Z)(function*(){if(t&&n)try{if((yield o.api.updateColor(t,n)).done){const l=o.labels.find(d=>d.id===t);l&&(l.color_code=n),o.cdr.markForCheck()}}catch(r){(0,b.tu)(r)}})()}searchLabels(t){this.filteredLabels=this.searchPipe.transform(this.labels,t||null),this.cdr.markForCheck()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(U.z),e.Y36(zt.u),e.Y36(Oe.g),e.Y36(e.sBO),e.Y36(te.h))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-labels"]],decls:14,vars:11,consts:[[3,"nzExtra"],[3,"nzActive","nzLoading","nzParagraph"],["nzPaginationType","small","nzSize","small",1,"custom-table",3,"nzData","nzLoading","nzNoResult"],["table",""],["scope","col","nzAlign","left",3,"nzWidth"],["scope","col","nzAlign","left","colspan","2"],["class","actions-row",4,"ngFor","ngForOf","ngForTrackBy"],["actionsTemplate",""],[1,"actions-row"],["nz-dropdown","","nz-tooltip","",1,"text-dark","m-0",2,"cursor","pointer",3,"nzColor","nzDropdownMenu","nzTooltipTitle","nzTrigger"],["colorDropdown","nzDropdownMenu"],["nz-menu","","nzSelectable","",2,"max-height","200px","overflow","hidden","overflow-y","auto"],["nz-menu-item","",3,"click",4,"ngFor","ngForOf"],["nzAlign","center",1,"actions-col"],[1,"actions"],["nz-button","","nz-popconfirm","","nz-tooltip","",3,"nzOkText","nzPopconfirmTitle","nzSize","nzTooltipPlacement","nzTooltipTitle","nzType","nzOnConfirm"],["nz-icon","","nzType","delete"],["nz-menu-item","",3,"click"],[1,"text-dark","m-0","w-100",3,"nzColor"],["nz-form","",3,"nzLayout",4,"nzSpaceItem"],[3,"key",4,"nzSpaceItem"],["nz-form","",3,"nzLayout"],[3,"nzSuffix"],["nz-input","","name","search","placeholder","Search by name","type","text",3,"ngModel","ngModelChange"],["suffixIconSearch",""],["nz-icon","","nzType","search"],[3,"key"]],template:function(t,n){if(1&t&&(e.TgZ(0,"nz-card",0)(1,"nz-skeleton",1)(2,"nz-table",2,3)(4,"thead")(5,"tr")(6,"th",4),e._uU(7,"Label"),e.qZA(),e.TgZ(8,"th",5),e._uU(9,"Associated tasks"),e.qZA()()(),e.TgZ(10,"tbody"),e.YNc(11,Ct,15,13,"tr",6),e.qZA()()()(),e.YNc(12,vt,3,0,"ng-template",null,7,e.W1O)),2&t){const o=e.MAs(3),r=e.MAs(13);e.Q6J("nzExtra",r),e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(10,wt)),e.xp6(1),e.Q6J("nzData",n.filteredLabels)("nzLoading",n.loading)("nzNoResult","Labels can be created while updating or creating tasks."),e.xp6(4),e.Q6J("nzWidth","300px"),e.xp6(5),e.Q6J("ngForOf",o.data)("ngForTrackBy",n.trackByFn)}},dependencies:[h.sg,V.bd,M.Ls,p._Y,p.Fj,p.JJ,p.JL,x.Lr,P.Zp,P.gB,P.ke,O.ix,v.w,L.dQ,z.N8,z.Uo,z._C,z.Om,z.p0,z.$Z,z.UX,T.wO,T.r9,H.NU,H.$1,J.SY,$.ng,p.On,p.F,Me.l,E.cm,E.RR,ze.j,Te.JW],changeDetection:0}),a})();var St=c(82803),Je=c(55522);function Pt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",12),e.NdJ("click",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw();return e.KtG(o.editTemplate(n.id))}),e._UZ(1,"span",13),e.qZA()}}function Mt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",14),e.NdJ("nzOnConfirm",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw();return e.KtG(o.deleteTemplate(n.id))}),e._UZ(1,"span",15),e.qZA()}}function Ot(i,a){if(1&i&&(e.TgZ(0,"tr",7)(1,"td"),e._uU(2),e.qZA(),e.TgZ(3,"td"),e._uU(4),e.ALo(5,"fromNow"),e.qZA(),e.TgZ(6,"td",8)(7,"div",9)(8,"nz-space"),e.YNc(9,Pt,2,0,"button",10),e.YNc(10,Mt,2,0,"button",11),e.qZA()()()()),2&i){const s=a.$implicit;e.xp6(2),e.Oqu(s.name),e.xp6(2),e.Oqu(e.lcZ(5,2,s.created_at))}}const At=function(){return{rows:5}};let It=(()=>{var i;class a{constructor(t,n,o,r,l){this.api=t,this.settingsApi=n,this.fb=o,this.app=r,this.menu=l,this.taskTemplates=[],this.loading=!1,this.updating=!1,this.drawerVisible=!1,this.editingTeamId=null,this.selectedTemplateId="",this.app.setTitle("Task Templates"),A.s.track(I.x0),this.form=this.fb.group({name:[null,p.kI.required]})}ngOnInit(){this.getTaskTemplates().then(t=>t)}getTaskTemplates(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.api.get();n.done&&(t.taskTemplates=n.body),t.loading=!1}catch(n){t.loading=!1,(0,b.tu)(n)}})()}closeModal(){this.drawerVisible=!1}editTemplate(t){t&&(this.selectedTemplateId=t,this.drawerVisible=!0)}taskTemplateCancel(t){this.drawerVisible=t,this.selectedTemplateId=""}onCreateOrUpdate(){this.getTaskTemplates().then(t=>t)}deleteTemplate(t=""){var n=this;return(0,m.Z)(function*(){try{(yield n.api.delete(t)).done&&(A.s.track(I.O1),n.getTaskTemplates())}catch{}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(St.e),e.Y36(he.G),e.Y36(p.qu),e.Y36(U.z),e.Y36(te.h))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-templates"]],decls:13,vars:9,consts:[[3,"nzActive","nzLoading","nzParagraph"],["nzPaginationType","small","nzSize","small",1,"custom-table",3,"nzData","nzLoading"],["table",""],["scope","col"],["colspan","2","nzAlign","left","scope","col"],["class","actions-row",4,"ngFor","ngForOf"],[3,"drawerVisible","selectedTemplateId","onCancelClick","onCreateOrUpdate"],[1,"actions-row"],["nzAlign","center",1,"actions-col"],[1,"actions"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click",4,"nzSpaceItem"],["nz-button","","nz-popconfirm","","nz-tooltip","","nzOkText","Yes","nzPopconfirmTitle","Are you sure?","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Delete","nzType","default",3,"nzOnConfirm",4,"nzSpaceItem"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click"],["nz-icon","","nzType","edit"],["nz-button","","nz-popconfirm","","nz-tooltip","","nzOkText","Yes","nzPopconfirmTitle","Are you sure?","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Delete","nzType","default",3,"nzOnConfirm"],["nz-icon","","nzType","delete"]],template:function(t,n){if(1&t&&(e.TgZ(0,"nz-card")(1,"nz-skeleton",0)(2,"nz-table",1,2)(4,"thead")(5,"tr")(6,"th",3),e._uU(7,"Name"),e.qZA(),e.TgZ(8,"th",4),e._uU(9,"Created"),e.qZA()()(),e.TgZ(10,"tbody"),e.YNc(11,Ot,11,4,"tr",5),e.qZA()()()(),e.TgZ(12,"worklenz-task-template-drawer",6),e.NdJ("onCancelClick",function(r){return n.taskTemplateCancel(r)})("onCreateOrUpdate",function(){return n.onCreateOrUpdate()}),e.qZA()),2&t){const o=e.MAs(3);e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(8,At)),e.xp6(1),e.Q6J("nzData",n.taskTemplates)("nzLoading",n.loading),e.xp6(9),e.Q6J("ngForOf",o.data),e.xp6(1),e.Q6J("drawerVisible",n.drawerVisible)("selectedTemplateId",n.selectedTemplateId)}},dependencies:[h.sg,V.bd,M.Ls,O.ix,v.w,L.dQ,z.N8,z.Uo,z._C,z.Om,z.p0,z.$Z,z.UX,H.NU,H.$1,J.SY,$.ng,Je.x,Te.JW,ge.d]}),a})();var G=c(71993),Zt=c(45538),Lt=c(3278),de=c(64532),Ft=c(8689),Ge=c(72095),De=c(96928);function Nt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",14),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.refresh())}),e._UZ(1,"span",15),e.qZA()}if(2&i){const s=e.oxw();e.xp6(1),e.Q6J("nzSpin",s.loading)}}function Et(i,a){1&i&&e._UZ(0,"span",20)}function Ut(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"form",16),e.NdJ("ngSubmit",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.search())}),e.TgZ(1,"nz-input-group",17),e._UZ(2,"input",18),e.qZA(),e.YNc(3,Et,1,0,"ng-template",null,19,e.W1O),e.qZA()}if(2&i){const s=e.MAs(4),t=e.oxw();e.Q6J("formGroup",t.searchForm)("nzLayout","vertical"),e.xp6(1),e.Q6J("nzSuffix",s),e.xp6(1),e.Q6J("formControlName","search")}}function Jt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"span")(1,"button",21),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.openAddMemberForm())}),e._uU(2,"Add Member"),e.qZA()()}}function Gt(i,a){if(1&i&&e._UZ(0,"nz-avatar",33),2&i){const s=e.oxw().$implicit,t=e.oxw();e.Udp("background-color",s.avatar_url?"#ececec":t.getColor(s.name)),e.Q6J("nzSize",28)("nzText",s.name.charAt(0).toUpperCase())("nzSrc",s.avatar_url)}}function Dt(i,a){1&i&&(e.TgZ(0,"span",34),e._uU(1," (Deactivated)"),e.qZA())}function Yt(i,a){if(1&i&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.Oqu(s.email)}}function Qt(i,a){1&i&&(e.TgZ(0,"span"),e._uU(1,"-"),e.qZA())}function Rt(i,a){if(1&i&&(e.TgZ(0,"span"),e._uU(1),e.TgZ(2,"small",35),e._uU(3,"(Pending Invitation)"),e.qZA()()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.hij(" ",s.email," ")}}function Bt(i,a){if(1&i&&(e.TgZ(0,"span",36),e._uU(1),e.qZA()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.Oqu(s.role_name)}}function Ht(i,a){if(1&i&&(e.TgZ(0,"span",37),e._uU(1),e.qZA()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.Oqu(s.role_name)}}function $t(i,a){if(1&i&&(e.TgZ(0,"span",38),e._uU(1),e.qZA()),2&i){const s=e.oxw().$implicit;e.xp6(1),e.Oqu(s.role_name)}}function jt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",42),e.NdJ("click",function(){e.CHM(s);const n=e.oxw(2).$implicit,o=e.oxw();return e.KtG(o.editMember(n.id))}),e._UZ(1,"span",43),e.qZA()}2&i&&e.Q6J("nzTooltipPlacement","top")("nzTooltipTitle","Edit")("nzType","default")}function qt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",45),e.NdJ("nzOnConfirm",function(){e.CHM(s);const n=e.oxw(3).$implicit,o=e.oxw();return e.KtG(o.toggleMemberActiveStatus(n))}),e._UZ(1,"span",46),e.qZA()}if(2&i){const s=e.oxw(3).$implicit;e.Q6J("nzOkText","Yes")("nzPopconfirmTitle","Are you sure?")("nzTooltipPlacement","top")("nzTooltipTitle",s.active?"Deactivate":"Activate")("nzType","default")}}function Vt(i,a){if(1&i&&(e.TgZ(0,"div"),e.YNc(1,qt,2,5,"button",44),e.qZA()),2&i){const s=e.oxw(2).$implicit,t=e.oxw();e.xp6(1),e.Q6J("ngIf",s.id!==(null==t.profile?null:t.profile.team_member_id))}}function Kt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",47),e.NdJ("nzOnConfirm",function(){e.CHM(s);const n=e.oxw(2).$implicit,o=e.oxw();return e.KtG(o.deleteTeamMember(n.id,n.email))}),e._UZ(1,"span",48),e.qZA()}2&i&&e.Q6J("nzOkText","Yes")("nzPopconfirmTitle","Are you sure?")("nzSize","small")("nzTooltipPlacement","top")("nzTooltipTitle","Delete")("nzType","default")}function Wt(i,a){1&i&&(e.TgZ(0,"div",39)(1,"nz-space"),e.YNc(2,jt,2,3,"button",40),e.YNc(3,Vt,2,1,"div",4),e.YNc(4,Kt,2,6,"button",41),e.qZA()())}function Xt(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"tr",22)(1,"td",23),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.selectMember(o.id))}),e.YNc(2,Gt,1,5,"nz-avatar",24),e.TgZ(3,"nz-badge",25),e._uU(4),e.qZA(),e.YNc(5,Dt,2,0,"span",26),e.qZA(),e.TgZ(6,"td",23),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.selectMember(o.id))}),e._uU(7),e.qZA(),e.TgZ(8,"td",23),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.selectMember(o.id))}),e.YNc(9,Yt,2,1,"span",27),e.YNc(10,Qt,2,0,"span",27),e.YNc(11,Rt,4,1,"span",27),e.qZA(),e.TgZ(12,"td",23),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.selectMember(o.id))}),e.YNc(13,Bt,2,1,"span",28),e.YNc(14,Ht,2,1,"span",29),e.YNc(15,$t,2,1,"span",30),e.qZA(),e.TgZ(16,"td",31),e.YNc(17,Wt,5,0,"div",32),e.qZA()()}if(2&i){const s=a.$implicit;e.xp6(2),e.Q6J("ngIf",s.name),e.xp6(1),e.Q6J("nzColor",s.is_online?"green":""),e.xp6(1),e.hij(" ",s.name," "),e.xp6(1),e.Q6J("ngIf",!s.active),e.xp6(2),e.Oqu(s.projects_count||0),e.xp6(2),e.Q6J("ngIf",s.email&&!s.pending_invitation),e.xp6(1),e.Q6J("ngIf",!s.email&&!s.pending_invitation),e.xp6(1),e.Q6J("ngIf",s.email&&s.pending_invitation),e.xp6(2),e.Q6J("ngIf",s.is_admin),e.xp6(1),e.Q6J("ngIf",s.is_owner),e.xp6(1),e.Q6J("ngIf",!s.is_admin&&!s.is_owner),e.xp6(2),e.Q6J("ngIf",!s.is_owner)}}const en=function(){return{rows:6}},tn=function(){return[]};let nn=(()=>{var i;class a{constructor(t,n,o,r,l,d,_,y){this.auth=t,this.api=n,this.jobTitlesApi=o,this.fb=r,this.app=l,this.router=d,this.utilsService=_,this.settingsService=y,this.model={},this.showTeamMemberModal=!1,this.loading=!1,this.selectedMemberId=null,this.total=0,this.pageSize=j.L8,this.pageIndex=1,this.paginationSizes=[5,10,15,20,50,100],this.sortField=null,this.sortOrder=null,this.app.setTitle("Team Members"),A.s.track(I.nW),this.searchForm=this.fb.group({search:[]}),this.searchForm.valueChanges.subscribe(()=>{this.search()}),this.settingsService.onNewMemberCreated.pipe((0,G.sL)()).subscribe(()=>{this.getTeamMembers()})}get profile(){return this.auth.getCurrentSession()}getTeamMembers(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.api.get(t.pageIndex,t.pageSize,t.sortField,t.sortOrder,t.searchForm.value.search);n.done&&(t.model=n.body,t.total=t.model.total||0,t.utilsService.handleLastIndex(t.total,t.model.data?.length||0,t.pageIndex,o=>{t.pageIndex=o,t.getTeamMembers()})),t.loading=!1}catch(n){(0,b.tu)(n),t.loading=!1}})()}editMember(t){this.selectedMemberId=t||null,this.showTeamMemberModal=!0}reset(){this.selectedMemberId=null}deleteTeamMember(t,n){var o=this;return(0,m.Z)(function*(){if(t&&n)try{(yield o.api.delete(t,n)).done&&(A.s.track(I.OA),yield o.getTeamMembers(),o.auth.getCurrentSession()?.team_member_id===t&&window.location.reload())}catch(r){(0,b.tu)(r)}})()}selectValue(t){t&&t.select()}search(){var t=this;return(0,m.Z)(function*(){t.searchForm.value.search&&A.s.track(I.Q5),t.getTeamMembers()})()}selectMember(t){t&&(A.s.track(I.uc),this.editMember(t))}getColor(t){return j.Lj[t?.charAt(0).toUpperCase()||"A"]}onQueryParamsChange(t){var n=this;return(0,m.Z)(function*(){const{pageSize:o,pageIndex:r,sort:l}=t;n.pageIndex=r,n.pageSize=o;const d=l.find(_=>null!==_.value);n.sortField=d&&d.key||null,n.sortOrder=d&&d.value||"descend",yield n.getTeamMembers()})()}openAddMemberForm(){this.showTeamMemberModal=!0,A.s.track(I.oo)}refresh(){A.s.track(I.r9),this.getTeamMembers()}handleOnCreateOrUpdate(t){1==t&&A.s.track(I.YC),this.getTeamMembers()}toggleMemberActiveStatus(t){var n=this;return(0,m.Z)(function*(){if(t.id)try{(yield n.api.toggleMemberActiveStatus(t.id,t.active,t.email)).done&&(t.active&&A.s.track(I.ZH),t.active||A.s.track(I.aY),yield n.getTeamMembers())}catch(o){(0,b.tu)(o)}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(w.e),e.Y36(Zt.B),e.Y36(Lt.i),e.Y36(p.qu),e.Y36(U.z),e.Y36(N.F0),e.Y36(de.F),e.Y36(Ft.g))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-team-members"]],decls:26,vars:22,consts:[[1,"container"],[1,"px-0",3,"nzGhost"],["nz-button","","nz-tooltip","","nzShape","circle","nzTooltipTitle","Refresh members","nzType","default",3,"click",4,"nzSpaceItem"],["nz-form","",3,"formGroup","nzLayout","ngSubmit",4,"nzSpaceItem"],[4,"nzSpaceItem"],[3,"nzActive","nzLoading","nzParagraph"],["nzShowSizeChanger","","nzSize","small",1,"custom-table",3,"nzData","nzFrontPagination","nzLoading","nzPageIndex","nzPageSizeOptions","nzPageSize","nzTotal","nzQueryParams"],["table",""],["nzColumnKey","name","scope","col",3,"nzSortFn"],["nzColumnKey","projects_count","scope","col",3,"nzSortFn"],["nzColumnKey","email","scope","col",3,"nzSortFn"],["colspan","2","nzAlign","left","nzColumnKey","role_name","scope","col",3,"nzSortFn"],["class","actions-row",4,"ngFor","ngForOf"],[3,"show","memberId","onCancel","onCreateOrUpdate","showChange"],["nz-button","","nz-tooltip","","nzShape","circle","nzTooltipTitle","Refresh members","nzType","default",3,"click"],["nz-icon","","nzTheme","outline","nzType","sync",3,"nzSpin"],["nz-form","",3,"formGroup","nzLayout","ngSubmit"],[3,"nzSuffix"],["nz-input","","placeholder","Search by name","type","text",3,"formControlName"],["suffixIconSearch",""],["nz-icon","","nzType","search"],["nz-button","","nzType","primary",3,"click"],[1,"actions-row"],[1,"cursor-pointer",3,"click"],["class","me-2",3,"nzSize","nzText","background-color","nzSrc",4,"ngIf"],[1,"d-inline-flex","align-items-center","flex-row-reverse",2,"color","inherit",3,"nzColor"],["nz-typography","","nzType","warning",4,"ngIf"],[4,"ngIf"],["class","admin-role","nz-typography","",4,"ngIf"],["class","owner-role","nz-typography","",4,"ngIf"],["class","member-role","nz-typography","",4,"ngIf"],[1,"actions-col"],["class","actions",4,"ngIf"],[1,"me-2",3,"nzSize","nzText","nzSrc"],["nz-typography","","nzType","warning"],["nz-typography","","nzType","secondary"],["nz-typography","",1,"admin-role"],["nz-typography","",1,"owner-role"],["nz-typography","",1,"member-role"],[1,"actions"],["nz-button","","nz-tooltip","","nzSize","small",3,"nzTooltipPlacement","nzTooltipTitle","nzType","click",4,"nzSpaceItem"],["nz-button","","nz-popconfirm","","nz-tooltip","",3,"nzOkText","nzPopconfirmTitle","nzSize","nzTooltipPlacement","nzTooltipTitle","nzType","nzOnConfirm",4,"nzSpaceItem"],["nz-button","","nz-tooltip","","nzSize","small",3,"nzTooltipPlacement","nzTooltipTitle","nzType","click"],["nz-icon","","nzType","edit"],["nz-button","","nz-tooltip","","nz-popconfirm","","nzSize","small",3,"nzOkText","nzPopconfirmTitle","nzTooltipPlacement","nzTooltipTitle","nzType","nzOnConfirm",4,"ngIf"],["nz-button","","nz-tooltip","","nz-popconfirm","","nzSize","small",3,"nzOkText","nzPopconfirmTitle","nzTooltipPlacement","nzTooltipTitle","nzType","nzOnConfirm"],["nz-icon","","nzType","user-switch","nzTheme","outline"],["nz-button","","nz-popconfirm","","nz-tooltip","",3,"nzOkText","nzPopconfirmTitle","nzSize","nzTooltipPlacement","nzTooltipTitle","nzType","nzOnConfirm"],["nz-icon","","nzType","delete"]],template:function(t,n){if(1&t&&(e.TgZ(0,"div",0)(1,"nz-page-header",1)(2,"nz-page-header-title"),e._uU(3),e.qZA(),e.TgZ(4,"nz-page-header-extra")(5,"nz-space"),e.YNc(6,Nt,2,1,"button",2),e.YNc(7,Ut,5,4,"form",3),e.YNc(8,Jt,3,0,"span",4),e.qZA()()(),e.TgZ(9,"nz-card")(10,"nz-skeleton",5)(11,"nz-table",6,7),e.NdJ("nzQueryParams",function(r){return n.onQueryParamsChange(r)}),e.TgZ(13,"thead")(14,"tr")(15,"th",8),e._uU(16,"Name"),e.qZA(),e.TgZ(17,"th",9),e._uU(18,"Projects"),e.qZA(),e.TgZ(19,"th",10),e._uU(20,"Email"),e.qZA(),e.TgZ(21,"th",11),e._uU(22,"Team Access"),e.qZA()()(),e.TgZ(23,"tbody"),e.YNc(24,Xt,18,12,"tr",12),e.qZA()()()()(),e.TgZ(25,"worklenz-team-members-form",13),e.NdJ("onCancel",function(){return n.reset()})("onCreateOrUpdate",function(r){return n.handleOnCreateOrUpdate(r)})("showChange",function(r){return n.showTeamMemberModal=r}),e.qZA()),2&t){const o=e.MAs(12);e.xp6(1),e.Q6J("nzGhost",!1),e.xp6(2),e.AsE("",n.total||0," Member",n.total>1?"s":"",""),e.xp6(7),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(20,en)),e.xp6(1),e.Q6J("nzData",n.model.data||e.DdM(21,tn))("nzFrontPagination",!1)("nzLoading",n.loading)("nzPageIndex",n.pageIndex)("nzPageSizeOptions",n.paginationSizes)("nzPageSize",n.pageSize)("nzTotal",n.total),e.xp6(4),e.Q6J("nzSortFn",!0),e.xp6(2),e.Q6J("nzSortFn",!0),e.xp6(2),e.Q6J("nzSortFn",!0),e.xp6(2),e.Q6J("nzSortFn",!0),e.xp6(3),e.Q6J("ngForOf",o.data),e.xp6(1),e.Q6J("show",n.showTeamMemberModal)("memberId",n.selectedMemberId)}},dependencies:[h.sg,h.O5,V.bd,Y.$O,Y.u9,Y.Jp,M.Ls,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,x.Lr,P.Zp,P.gB,P.ke,O.ix,v.w,L.dQ,z.N8,z.qD,z.Uo,z._C,z.Om,z.p0,z.$Z,z.UX,Q.ZU,H.NU,H.$1,J.SY,$.ng,ie.x7,Te.JW,Ge.S,De.Dz]}),a})();var sn=c(80697);function on(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",10),e.NdJ("click",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw();return e.KtG(o.editTemplate(n.id,n.name))}),e._UZ(1,"span",11),e.qZA()}}function an(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",12),e.NdJ("nzOnConfirm",function(){e.CHM(s);const n=e.oxw().$implicit,o=e.oxw();return e.KtG(o.deleteTemplate(n.id))}),e._UZ(1,"span",13),e.qZA()}}function rn(i,a){if(1&i&&(e.TgZ(0,"tr",5)(1,"td"),e._uU(2),e.qZA(),e.TgZ(3,"td",6)(4,"div",7)(5,"nz-space"),e.YNc(6,on,2,0,"button",8),e.YNc(7,an,2,0,"button",9),e.qZA()()()()),2&i){const s=a.$implicit;e.xp6(2),e.Oqu(s.name)}}const ln=function(){return{rows:5}};let cn=(()=>{var i;class a{constructor(t,n,o){this.router=t,this.cdr=n,this.api=o,this.loading=!1,this.projectTemplates=[]}ngOnInit(){this.get()}get(){var t=this;return(0,m.Z)(function*(){try{t.loading=!0;const n=yield t.api.getWorklenzCustomTemplates();n.done&&(t.projectTemplates=n.body,t.loading=!1,t.cdr.markForCheck()),t.loading=!1,t.cdr.markForCheck()}catch(n){(0,b.tu)(n),t.cdr.markForCheck()}})()}editTemplate(t,n){!t||!n||this.router.navigate([`/worklenz/settings/project-templates/edit/${t}/${n}`])}deleteTemplate(t){var n=this;return(0,m.Z)(function*(){if(t)try{(yield n.api.delete(t)).done&&n.get()}catch(o){(0,b.tu)(o)}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(N.F0),e.Y36(e.sBO),e.Y36(sn.I))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-project-templates"]],decls:11,vars:7,consts:[[3,"nzActive","nzLoading","nzParagraph"],["nzPaginationType","small","nzSize","small",1,"custom-table",3,"nzData","nzLoading"],["table",""],["scope","col"],["class","actions-row",4,"ngFor","ngForOf"],[1,"actions-row"],["nzAlign","center",1,"actions-col"],[1,"actions"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click",4,"nzSpaceItem"],["nz-button","","nz-popconfirm","","nz-tooltip","","nzOkText","Yes","nzPopconfirmTitle","Are you sure?","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Delete","nzType","default",3,"nzOnConfirm",4,"nzSpaceItem"],["nz-button","","nz-tooltip","","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Edit","nzType","default",3,"click"],["nz-icon","","nzType","edit"],["nz-button","","nz-popconfirm","","nz-tooltip","","nzOkText","Yes","nzPopconfirmTitle","Are you sure?","nzSize","small","nzTooltipPlacement","top","nzTooltipTitle","Delete","nzType","default",3,"nzOnConfirm"],["nz-icon","","nzType","delete"]],template:function(t,n){if(1&t&&(e.TgZ(0,"nz-card")(1,"nz-skeleton",0)(2,"nz-table",1,2)(4,"thead")(5,"tr")(6,"th",3),e._uU(7,"Name"),e.qZA(),e._UZ(8,"th",3),e.qZA()(),e.TgZ(9,"tbody"),e.YNc(10,rn,8,1,"tr",4),e.qZA()()()()),2&t){const o=e.MAs(3);e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(6,ln)),e.xp6(1),e.Q6J("nzData",n.projectTemplates)("nzLoading",n.loading),e.xp6(8),e.Q6J("ngForOf",o.data)}},dependencies:[h.sg,V.bd,M.Ls,O.ix,v.w,L.dQ,z.N8,z.Uo,z._C,z.Om,z.p0,z.$Z,z.UX,H.NU,H.$1,J.SY,$.ng,Te.JW],changeDetection:0}),a})();var Ce=c(26236),k=c(94489),R=c(78645),pn=c(65619),q=c(975);let ue=(()=>{var i;class a{constructor(){this._selectSbj$=new R.x,this._deselectSbj$=new R.x,this._deselectAllSbj$=new R.x,this._groupTaskMap=new Map,this._taskGroupIdsMap=new Map,this._selectedTasksMap=new Map,this._allTasksMap=new Map,this._subTasksMap=new Map,this._selectedCount=0}get tasks(){return this._allTasksMap}get onSelect$(){return this._selectSbj$.asObservable()}get onDeselect$(){return this._deselectSbj$.asObservable()}get onDeselectAll$(){return this._deselectAllSbj$.asObservable()}reset(){this._groupTaskMap.clear(),this._taskGroupIdsMap.clear(),this._selectedTasksMap.clear(),this._allTasksMap.clear(),this._subTasksMap.clear(),this._selectedCount=0}registerGroup(t){for(const n of t.tasks)this.add(t.id,n)}add(t,n){n.id&&(this.updateGroupTaskMap(t,n.id),this._taskGroupIdsMap.set(n.id,t),this._allTasksMap.set(n.id,n),n.parent_task_id&&this.updateSubtasksMap(n.parent_task_id,n))}addGroupTask(t,n){n.id&&this._taskGroupIdsMap.set(n.id,t)}has(t){return this._allTasksMap.has(t)}remove(t){t.id&&(this.deselectTask(t),this._taskGroupIdsMap.get(t.id),this._allTasksMap.delete(t.id))}updateGroupTaskMap(t,n,o){const r=this._groupTaskMap.get(t);r?("boolean"==typeof o?r[n]=o:delete r[n],this._groupTaskMap.set(t,r)):this._groupTaskMap.set(t,{[n]:o||!1})}updateSubtasksMap(t,n,o){const r=this._subTasksMap.get(t)||[];r.some(d=>d.id===n.id)||(r.push(n),this._subTasksMap.set(t,r))}selectTask(t){this._selectedTasksMap.get(t.id)||(this._selectedTasksMap.set(t.id,t),this._selectedCount++,this.updateGroupTaskMap(this._taskGroupIdsMap.get(t.id),t.id,!0),this._selectSbj$.next(t))}deselectTask(t){this._selectedTasksMap.has(t.id)&&(this._selectedTasksMap.delete(t.id),this._selectedCount--,this.updateGroupTaskMap(this._taskGroupIdsMap.get(t.id),t.id,!1),this._deselectSbj$.next(t))}deselectLocalGroups(){for(const[t,n]of this._groupTaskMap)for(const o in n)this.updateGroupTaskMap(t,o,!1)}deselectAll(){this._selectedTasksMap.size&&(this.deselectLocalGroups(),this._selectedTasksMap.clear(),this._selectedCount=0,this._deselectAllSbj$.next())}isAllSelected(t){const n=this._groupTaskMap.get(t);if(n){for(const o in n)if(!n[o])return!1;return!0}return!1}isAllDeselected(t){const n=this._groupTaskMap.get(t);if(n)for(const o in n)if(n[o])return!1;return!0}getSelectedCount(){return this._selectedCount}getGroupId(t){return this._taskGroupIdsMap.get(t)}getSelectedTasks(){const t=[];for(const[,n]of this._selectedTasksMap.entries())t.push(n);return t}getSelectedTaskIds(){const t=[];for(const[n]of this._selectedTasksMap.entries())t.push(n);return t}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),B=(()=>{var i;class a{get _currentGroup(){const t=localStorage.getItem("worklenz.pt-t-list.group_by");if(t){const n=this.GROUP_BY_OPTIONS.find(o=>o.value===t);if(n)return n}return this.GROUP_BY_OPTIONS[0]}set _currentGroup(t){localStorage.setItem("worklenz.pt-t-list.group_by",t.value)}set columns(t){this._cols=t,this.emitColsChange()}get columns(){return this._cols}set labels(t){this._labels=t,this.labelsSbj$.next()}get labels(){return this._labels}set priorities(t){this._priorities=t,this.prioritiesSbj$.next()}get priorities(){return this._priorities}set phases(t){this._phases=t,this.phasesSbj$.next()}get phases(){return this._phases}get onColumnsChange$(){return this.colsSbj$.asObservable()}get onLabelsChange$(){return this.labelsSbj$.asObservable()}get onStatusesChange$(){return this.statusesSbj$.asObservable()}get onPrioritiesChange$(){return this.prioritiesSbj$.asObservable()}get onContextMenu$(){return this.contextMenuSbj$.asObservable()}get onTaskAddOrDelete$(){return this.taskAddOrDeleteSbj$.asObservable()}get onGroupChange$(){return this.groupChangeSbj$.asObservable()}get onRefresh$(){return this.refreshSbj$.asObservable()}get onPhaseChange$(){return this.phasesSbj$.asObservable()}set statuses(t){this._statuses=t,this.statusesSbj$.next()}get statuses(){return this._statuses}get onRefreshSubtasksIncluded(){return this.refreshSubtasksIncludedSbj$.asObservable()}constructor(t,n){this.socket=t,this.map=n,this.colsSbj$=new R.x,this.labelsSbj$=new R.x,this.statusesSbj$=new R.x,this.prioritiesSbj$=new R.x,this.contextMenuSbj$=new R.x,this.taskAddOrDeleteSbj$=new pn.X(null),this.refreshSbj$=new R.x,this.groupChangeSbj$=new R.x,this.phasesSbj$=new R.x,this.updateGroupProgressSbj$=new R.x,this.refreshSubtasksIncludedSbj$=new R.x,this.HIGHLIGHT_COL_CLS="highlight-col",this.GROUP_BY_STATUS_VALUE="status",this.GROUP_BY_PRIORITY_VALUE="priority",this.GROUP_BY_PHASE_VALUE="phase",this.GROUP_BY_OPTIONS=[{label:"Status",value:this.GROUP_BY_STATUS_VALUE},{label:"Priority",value:this.GROUP_BY_PRIORITY_VALUE},{label:"Phase",value:this.GROUP_BY_PHASE_VALUE}],this.groups=[],this._templateId=null,this._cols=[],this._labels=[],this._statuses=[],this._priorities=[],this._phases=[],this.isSubtasksIncluded=!1}settemplateId(t){this._templateId=t}gettemplateId(){return this._templateId}setCurrentGroup(t){this._currentGroup=t}getCurrentGroup(){return this._currentGroup}emitColsChange(){this.colsSbj$.next()}emitOnContextMenu(t,n){this.contextMenuSbj$.next({event:t,task:n})}emitRefresh(){this.refreshSbj$.next()}emitGroupChange(t,n,o){this.groupChangeSbj$.next({groupId:t,taskId:n,color:o})}emitTaskAddOrDelete(t,n){this.taskAddOrDeleteSbj$.next({taskId:t,isSubTask:n})}emitRefreshSubtasksIncluded(){this.refreshSubtasksIncludedSbj$.next()}getGroupIdByGroupedColumn(t){const n=this.getCurrentGroup().value;return n===this.GROUP_BY_STATUS_VALUE?t.status:n===this.GROUP_BY_PRIORITY_VALUE?t.priority:n===this.GROUP_BY_PHASE_VALUE?t.phase_id:null}updateTaskGroup(t,n=!0){if(!t.id)return;const o=this.getGroupIdByGroupedColumn(t);o&&(this.deleteTask(t.id),this.addTask(t,o,n))}deleteTask(t,n=null){const o=this.map.getGroupId(t);if(!o||!t)return;const r=this.groups.find(d=>d.id===o);if(!r)return;const l=this.map.getSelectedTasks().find(d=>d.id===t);if(l?.is_sub_task){const d=r.tasks.find(_=>_.id===l.parent_task_id);if(d){const _=d.sub_tasks?.findIndex(y=>y.id===l.id);typeof _<"u"&&-1!==_&&(d.sub_tasks_count||(d.sub_tasks_count=0),d.sub_tasks_count=Math.max(+d.sub_tasks_count-1,0),d.sub_tasks?.splice(_,1),this.emitTaskAddOrDelete(d.id,!0))}this.map.remove(l)}else{const d=n??r.tasks.findIndex(_=>_.id===t);-1!==d&&(this.map.remove(r.tasks[d]),r.tasks.splice(d,1),this.emitTaskAddOrDelete(t,!1))}this.map.deselectAll()}removeSubtask(t,n=null){const o=this.map.getGroupId(t);if(!o||!t)return;const r=this.groups.find(d=>d.id===o);if(!r)return;const l=n??r.tasks.findIndex(d=>d.id===t);-1!==l&&(this.map.remove(r.tasks[l]),r.tasks.splice(l,1)),this.map.deselectAll()}addTask(t,n,o=!1){const r=this.groups.find(l=>l.id===n);if(r&&t.id){if(t.parent_task_id){const l=r.tasks.find(d=>d.id===t.parent_task_id);l&&(l.sub_tasks_count||(l.sub_tasks_count=0),l.sub_tasks_count=+l.sub_tasks_count+1,l.sub_tasks?.push(t))}else o?r.tasks.unshift(t):r.tasks.push(t);this.map.add(n,t),this.emitTaskAddOrDelete(t.parent_task_id,!!t.parent_task_id)}}reset(){this._cols=[],this._labels=[],this._statuses=[],this._priorities=[],this._templateId=null,this.groups=[],this.isSubtasksIncluded=!1}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(q.s),e.LFG(ue))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),Ye=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/pt-tasks`}getTaskList(t){const n=(0,b.UK)(t);return this._get(this.http,`${this.root}/list/${t.id}${n}`)}bulkDelete(t,n){return this._put(this.http,`${this.root}/bulk/delete`,t)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),dn=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/labels`}get(t=null){const n=(0,b.UK)({project:t});return this._get(this.http,`${this.root}${n}`)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),Ae=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/pt-statuses`}create(t){return this._post(this.http,this.root,t)}get(t){return this._get(this.http,`${this.root}?template_id=${t}`)}getById(t){return this._get(this.http,`${this.root}/${t}`)}getCategories(){return this._get(this.http,`${this.root}/categories`)}update(t,n){return this._put(this.http,`${this.root}/${t}`,n)}updateName(t,n){return this._put(this.http,`${this.root}/name/${t}`,n)}delete(t,n,o){return this._delete(this.http,`${this.root}/${t}?project=${n}&replace=${o||null}`)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),un=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/task-priorities`}get(){return this._get(this.http,this.root)}getById(t){return this._get(this.http,`${this.root}/${t}`)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})(),Ie=(()=>{var i;class a extends le.P{constructor(t){super(),this.http=t,this.root=`${this.API_BASE_URL}/pt-task-phases`}create(t){const n=(0,b.UK)({id:t});return this._post(this.http,`${this.root}${n}`,{})}get(t){const n=(0,b.UK)({id:t});return this._get(this.http,`${this.root}${n}`)}getById(t,n){const o=(0,b.UK)({id:n});return this._get(this.http,`${this.root}/${t}${o}`)}updateLabel(t,n){return this._put(this.http,`${this.root}/label/${t}`,{name:n})}update(t,n){const o=(0,b.UK)({id:t});return this._put(this.http,`${this.root}/${n.id}${o}`,n)}updateColor(t,n){const o=(0,b.UK)({id:t,current_project_id:t});return this._put(this.http,`${this.root}/change-color/${n.id}${o}`,n)}delete(t,n){const o=(0,b.UK)({id:n});return this._delete(this.http,`${this.root}/${t}${o}`)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(ce.eN))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})();const _n=["taskInput"];function mn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"form",3)(1,"input",4,5),e.NdJ("blur",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.onInputBlur())})("keyup.enter",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.addInstantTask())}),e.qZA()()}if(2&i){const s=e.oxw();e.Q6J("formGroup",s.form),e.xp6(1),e.Q6J("nzBorderless",!0)("formControlName","name")("readOnly",s.creating)}}function hn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",6),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.focusTaskInput())}),e._UZ(1,"span",7),e.TgZ(2,"span"),e._uU(3),e.qZA()()}if(2&i){const s=e.oxw();e.uIk("id",s.id),e.xp6(1),e.Q6J("nzType",s.creating?"loading":"plus")("nzTheme","outline"),e.xp6(2),e.hij(" ",s.label,"")}}let Qe=(()=>{var i;class a{constructor(t,n,o,r,l,d,_){this.socket=t,this.auth=n,this.fb=o,this.service=r,this.ngZone=l,this.cdr=d,this.map=_,this.subTaskInput=!1,this.templateId=null,this.parentTask=null,this.groupId=null,this.label="Add Task",this.focusChange=new e.vpe,this.taskInputVisible=!1,this.creating=!1,this.id=(0,b.q$)(4),this._session=null,this.form=this.fb.group({name:[null,[p.kI.required,p.kI.pattern(/^(\s+\S+\s*)*(?!\s).*$/)]]}),this._session=this.auth.getCurrentSession()}focusTaskInput(){this.taskInputVisible=!0,this.focusChange.emit(this.taskInputVisible),this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{this.taskInput?.nativeElement.focus(),this.taskInput?.nativeElement.select()},100)})}addTaskInputBlur(){this.taskInputVisible=!1,this.focusChange.emit(this.taskInputVisible)}onInputBlur(){var t=this;return(0,m.Z)(function*(){t.isValidInput()?yield t.addInstantTask():t.addTaskInputBlur()})()}createRequest(){if(!this.templateId||!this._session)return null;const t=this._session,n={name:this.form.value.name,template_id:this.templateId,reporter_id:t.id,team_id:t.team_id},o=this.service.getCurrentGroup();return o.value===this.service.GROUP_BY_STATUS_VALUE?n.status_id=this.groupId||void 0:o.value===this.service.GROUP_BY_PRIORITY_VALUE?n.priority_id=this.groupId||void 0:o.value===this.service.GROUP_BY_PHASE_VALUE&&(n.phase_id=this.groupId||void 0),this.parentTask&&(n.parent_task_id=this.parentTask),n}isValidInput(){return this.form.valid&&this.form.value.name.trim().length}addInstantTask(){var t=this;return(0,m.Z)(function*(){if(!t.creating&&t.templateId&&t._session&&t.isValidInput()){try{const n=t.createRequest();if(!n)return;t.creating=!0,t.socket.emit(k.C.PT_QUICK_TASK.toString(),JSON.stringify(n)),t.socket.once(k.C.PT_QUICK_TASK.toString(),o=>{t.creating=!1,t.onNewTaskReceived(o)})}catch{t.creating=!1}t.cdr.markForCheck()}})()}reset(t=!0){this.creating=!1,this.form.controls.name.setValue(null),this.taskInputVisible=!0,this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{this.taskInput?.nativeElement.focus(),t&&window.scrollTo(0,document.body.scrollHeight)},j.GR)}),this.cdr.markForCheck()}onNewTaskReceived(t){if(this.groupId&&t.id){if(this.map.has(t.id))return;this.service.addTask(t,this.groupId),this.reset(!1)}}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(q.s),e.Y36(w.e),e.Y36(p.qu),e.Y36(B),e.Y36(e.R0b),e.Y36(e.sBO),e.Y36(ue))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-add-task-input"]],viewQuery:function(t,n){if(1&t&&e.Gf(_n,5),2&t){let o;e.iGM(o=e.CRH())&&(n.taskInput=o.first)}},inputs:{subTaskInput:"subTaskInput",templateId:"templateId",parentTask:"parentTask",groupId:"groupId",label:"label"},outputs:{focusChange:"focusChange"},standalone:!0,features:[e.jDz],decls:3,vars:3,consts:[[3,"ngSwitch"],[3,"formGroup",4,"ngSwitchCase"],["class","editable-row d-block w-25 task-name",3,"click",4,"ngSwitchCase"],[3,"formGroup"],["nz-input","","type","text","tabindex","1","placeholder","Type your task and hit enter",3,"nzBorderless","formControlName","readOnly","blur","keyup.enter"],["taskInput",""],[1,"editable-row","d-block","w-25","task-name",3,"click"],["nz-icon","",1,"input-icon",3,"nzType","nzTheme"]],template:function(t,n){1&t&&(e.ynx(0,0),e.YNc(1,mn,3,4,"form",1),e.YNc(2,hn,4,4,"div",2),e.BQk()),2&t&&(e.Q6J("ngSwitch",n.taskInputVisible),e.xp6(1),e.Q6J("ngSwitchCase",!0),e.xp6(1),e.Q6J("ngSwitchCase",!1))},dependencies:[h.ez,h.RF,h.n9,p.UX,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,P.o7,P.Zp,M.PV,M.Ls],changeDetection:0}),a})();var gn=c(49278),Ze=c(63019),_e=c(76271);function fn(i,a){1&i&&e._UZ(0,"span",12),2&i&&e.Q6J("nzType","loading")("nzTheme","outline")}function zn(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"input",10),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.group.name=n)})("keydown.enter",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.updateName(n.group))})("blur",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.updateName(n.group))}),e.qZA(),e.YNc(2,fn,1,2,"span",11),e.BQk()}if(2&i){const s=e.oxw();e.xp6(1),e.ekj("bg-and-grey",!s.isEditColProgress),e.Q6J("id","group-name-"+s.group.id)("disabled",s.isEditColProgress)("ngModel",s.group.name),e.xp6(1),e.Q6J("ngIf",s.isEditColProgress)}}function Tn(i,a){if(1&i&&(e.ynx(0),e._uU(1),e.BQk()),2&i){const s=e.oxw();e.xp6(1),e.AsE(" ",s.group.name," (",s.group.tasks.length,") ")}}function Cn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",13),e.NdJ("nzVisibleChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.showMenu=n)}),e._UZ(1,"span",14),e.qZA()}if(2&i){e.oxw();const s=e.MAs(9);e.Q6J("nzType","text")("nzTrigger","click")("nzDropdownMenu",s),e.xp6(1),e.Q6J("nzType","ellipsis")("nzTheme","outline")}}function kn(i,a){if(1&i&&(e.TgZ(0,"span"),e._uU(1),e.qZA()),2&i){const s=e.oxw().$implicit,t=e.oxw(3);e.Udp("font-weight",s.id===t.group.category_id?"bold":null),e.xp6(1),e.hij(" ",(null==s?null:s.name)||null," ")}}function xn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",21),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw(3);return e.KtG(r.changeStatusCategory(r.group,o.id))}),e.ALo(1,"safeString"),e._UZ(2,"nz-badge",22),e.ALo(3,"safeString"),e.YNc(4,kn,2,3,"ng-template",null,23,e.W1O),e.qZA()}if(2&i){const s=a.$implicit,t=e.MAs(5);e.Q6J("nzTooltipTitle",e.lcZ(1,4,s.description))("nzTooltipPlacement","right"),e.xp6(2),e.Q6J("nzColor",e.lcZ(3,6,s.color_code))("nzText",t)}}function bn(i,a){if(1&i&&(e.TgZ(0,"li",19)(1,"ul"),e.YNc(2,xn,6,8,"li",20),e.qZA()()),2&i){const s=e.oxw(2),t=e.MAs(12);e.Q6J("nzTitle",t),e.xp6(2),e.Q6J("ngForOf",s.categories)}}function vn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"ul",15)(1,"li",16),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.editGroupName())}),e._UZ(2,"span",17),e._uU(3," Rename "),e.qZA(),e.YNc(4,bn,3,2,"li",18),e.qZA()}if(2&i){const s=e.oxw();e.xp6(4),e.Q6J("ngIf",s.isGroupByStatus)}}function wn(i,a){1&i&&(e._UZ(0,"span",24),e._uU(1," Change category\n"))}let yn=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.cdr=t,this.auth=n,this.statusApi=o,this.list=r,this.ngZone=l,this.phaseApi=d,this.templateId=null,this.categories=[],this.toggle=new e.vpe,this.onCreateOrUpdate=new e.vpe,this.edit=!1,this.isEditColProgress=!1,this.showMenu=!1,this.isGroupByStatus=!1,this.isGroupByPhases=!1,this.isAdmin=!1,this.handleGroupProgressChange=()=>{this.group&&this.cdr.markForCheck()},(0,Ze.T)(this.list.onGroupChange$,this.list.onTaskAddOrDelete$).pipe((0,G.sL)()).subscribe(()=>{this.handleGroupProgressChange()})}ngOnInit(){this.isGroupByStatus=this.list.getCurrentGroup().value===this.list.GROUP_BY_STATUS_VALUE,this.isGroupByPhases=this.list.getCurrentGroup().value===this.list.GROUP_BY_PHASE_VALUE;const t=this.auth.getCurrentSession();t&&(this.isAdmin=!(!t.owner&&!t.is_admin))}canDisplayActions(){const t=this.list.getCurrentGroup().value;return t!==this.list.GROUP_BY_PRIORITY_VALUE&&(this.isAdmin||this.isGroupByStatus||t===this.list.GROUP_BY_PHASE_VALUE)&&this.group.name!==j.k9}changeStatusCategory(t,n){var o=this;return(0,m.Z)(function*(){n&&(t.category_id=n,yield o.onBlurEditColumn(t),o.list.emitRefresh())})()}editGroupName(){this.edit=!0,this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{const n=document.querySelector(`#group-name-${this.group.id}`);n&&(n.focus(),n.select())})})}onToggleClick(t){this.edit||this.toggle.emit(t)}onBlurEditColumn(t){var n=this;return(0,m.Z)(function*(){if(n.templateId&&!n.isEditColProgress){try{n.isEditColProgress=!0;const o=n.list.getCurrentGroup().value;o===n.list.GROUP_BY_STATUS_VALUE?yield n.update(t):o===n.list.GROUP_BY_PHASE_VALUE&&(yield n.updatePhase(t)),n.isEditColProgress=!1,n.edit=!1}catch(o){(0,b.tu)(o),n.isEditColProgress=!1}n.cdr.markForCheck()}})()}updateName(t){var n=this;return(0,m.Z)(function*(){if(n.templateId&&!n.isEditColProgress){try{n.isEditColProgress=!0;const o=n.list.getCurrentGroup().value;o===n.list.GROUP_BY_STATUS_VALUE?yield n.updateGroupName(t):o===n.list.GROUP_BY_PHASE_VALUE&&(yield n.updatePhase(t)),n.isEditColProgress=!1,n.edit=!1}catch(o){(0,b.tu)(o),n.isEditColProgress=!1}n.cdr.markForCheck()}})()}updateGroupName(t){var n=this;return(0,m.Z)(function*(){if(t?.id&&n.templateId){try{const o={name:t.name,template_id:n.templateId,category_id:t.category_id},r=yield n.statusApi.updateName(t.id,o);if(r.done){const l=n.list.groups,d=l.find(_=>_.id===r.body.id);d&&(n.group.name=d.name=r.body.name||"",n.group.color_code=d.color_code=r.body.color_code||""),n.list.groups=l}}catch{}n.cdr.markForCheck()}})()}updatePhase(t){var n=this;return(0,m.Z)(function*(){if(t?.id&&n.templateId){try{const o={id:t.id,name:t.name},r=yield n.phaseApi.update(n.templateId,o);if(r.done){const l=n.list.phases,d=l.find(_=>_.id===r.body.id);d&&(n.group.name=d.name=r.body.name,n.group.color_code=d.color_code=r.body.color_code),n.list.phases=l}}catch{}n.cdr.markForCheck()}})()}update(t){var n=this;return(0,m.Z)(function*(){if(!n.isAdmin)return;const o={name:t.name,template_id:n.templateId,category_id:t.category_id},r=yield n.statusApi.update(t.id,o);r.done&&null!=r.body.color_code&&(t.color_code=r.body.color_code+j.Yj,n.onCreateOrUpdate.emit()),n.cdr.markForCheck()})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(w.e),e.Y36(Ae),e.Y36(B),e.Y36(e.R0b),e.Y36(Ie))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-list-group-settings"]],inputs:{group:"group",templateId:"templateId",categories:"categories"},outputs:{toggle:"toggle",onCreateOrUpdate:"onCreateOrUpdate"},decls:13,vars:11,consts:[[1,"d-flex","justify-content-between","align-items-center","position-relative"],[1,"d-flex","align-items-center"],["nz-button","",1,"collapse","btn","border-0",3,"click"],["nz-icon","",1,"collapse-icon",3,"nzType","nzTheme"],[3,"ngSwitch"],[4,"ngSwitchCase"],["nz-button","","class","p-0","nz-dropdown","",3,"nzType","nzTrigger","nzDropdownMenu","nzVisibleChange",4,"ngIf"],["menu","nzDropdownMenu"],["nz-menu","",4,"ngIf"],["titleTemplate",""],["nz-input","",1,"p-0",3,"id","disabled","ngModel","ngModelChange","keydown.enter","blur"],["nz-icon","","class","ms-2",3,"nzType","nzTheme",4,"ngIf"],["nz-icon","",1,"ms-2",3,"nzType","nzTheme"],["nz-button","","nz-dropdown","",1,"p-0",3,"nzType","nzTrigger","nzDropdownMenu","nzVisibleChange"],["nz-icon","",3,"nzType","nzTheme"],["nz-menu",""],["nz-menu-item","",3,"click"],["nz-icon","","nzType","edit","nzTheme","outline",1,"me-2"],["nz-submenu","",3,"nzTitle",4,"ngIf"],["nz-submenu","",3,"nzTitle"],["class","m-0","nz-tooltip","","nz-menu-item","",3,"nzTooltipTitle","nzTooltipPlacement","click",4,"ngFor","ngForOf"],["nz-tooltip","","nz-menu-item","",1,"m-0",3,"nzTooltipTitle","nzTooltipPlacement","click"],[3,"nzColor","nzText"],["textTmpl",""],["nz-icon","","nzType","retweet","nzTheme","outline",1,"me-2"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"div",1)(2,"button",2),e.NdJ("click",function(r){return n.onToggleClick(r)}),e._UZ(3,"span",3),e.ynx(4,4),e.YNc(5,zn,3,6,"ng-container",5),e.YNc(6,Tn,2,2,"ng-container",5),e.BQk(),e.qZA(),e.YNc(7,Cn,2,5,"button",6),e.qZA()(),e.TgZ(8,"nz-dropdown-menu",null,7),e.YNc(10,vn,5,1,"ul",8),e.qZA(),e.YNc(11,wn,2,0,"ng-template",null,9,e.W1O)),2&t&&(e.xp6(2),e.Udp("background-color",n.group.color_code),e.ekj("active",n.group.tasks.length),e.xp6(1),e.Q6J("nzType","right")("nzTheme","outline"),e.xp6(1),e.Q6J("ngSwitch",n.edit),e.xp6(1),e.Q6J("ngSwitchCase",!0),e.xp6(1),e.Q6J("ngSwitchCase",!1),e.xp6(1),e.Q6J("ngIf",n.canDisplayActions()),e.xp6(3),e.Q6J("ngIf",n.showMenu))},dependencies:[h.sg,h.O5,h.RF,h.n9,M.Ls,p.Fj,p.JJ,P.Zp,O.ix,v.w,L.dQ,T.wO,T.r9,T.rY,J.SY,p.On,ie.x7,E.cm,E.RR,E.wA,_e.m],styles:[".collapse[_ngcontent-%COMP%]{color:hwb(0 0% 100%/.85);font-weight:500;padding:6px 13px 6px 12px;min-width:120px;width:auto;border:none;text-align:left;outline:none;border-top-right-radius:4px;border-top-left-radius:4px;-webkit-user-select:none;user-select:none;z-index:8;font-size:14px;height:30px;display:flex;align-items:center}.collapse.btn[_ngcontent-%COMP%] .collapse-icon[_ngcontent-%COMP%]{transition:transform .1s;transform:rotate(0)}.collapse.btn.active[_ngcontent-%COMP%] .collapse-icon[_ngcontent-%COMP%]{transition:transform .1s;transform:rotate(90deg)}.collapse.active[_ngcontent-%COMP%]{border-bottom-left-radius:0;border-bottom-right-radius:0}.collapse[_ngcontent-%COMP%]:after{color:#777;font-weight:700;float:left;margin-left:5px}"],changeDetection:0}),a})();var Re=c(48327),we=c(62612),Le=c(44889);function Sn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",13),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.onPhaseSettingsClick())}),e._UZ(1,"span",14),e.qZA()}2&i&&(e.Q6J("nzShape","circle")("nzSize","small")("nzType","text"),e.xp6(1),e.Q6J("nzType","setting")("nzTheme","outline"))}let Pn=(()=>{var i;class a{get phaseLabel(){return this.phasesService.label}constructor(t,n,o,r,l){this.service=t,this.map=n,this.cdr=o,this.phasesService=r,this.auth=l,this.headerCls="flex-table header",this.selectChange=new e.vpe,this.phaseSettingsClick=new e.vpe,this.checked=!1,this.indeterminate=!1,this.map.onDeselectAll$.pipe((0,G.sL)()).subscribe(()=>{this.checked=!1,this.indeterminate=!1,this.cdr.markForCheck()}),this.phasesService.onLabelChange.pipe((0,G.sL)()).subscribe(d=>{this.cdr.markForCheck()}),(0,Ze.T)(this.map.onSelect$,this.map.onDeselect$).pipe((0,G.sL)()).subscribe(()=>{this.map.isAllDeselected(this.groupId)?(this.checked=!1,this.indeterminate=!1):this.map.isAllSelected(this.groupId)?(this.checked=!0,this.indeterminate=!1):this.indeterminate=!0,this.cdr.markForCheck()})}onAllChecked(t){this.selectChange?.emit(t)}onPhaseSettingsClick(){this.phaseSettingsClick.emit()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(B),e.Y36(ue),e.Y36(e.sBO),e.Y36(Re.u),e.Y36(w.e))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-list-header"]],hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.headerCls)},inputs:{groupId:"groupId"},outputs:{selectChange:"selectChange",phaseSettingsClick:"phaseSettingsClick"},decls:27,vars:8,consts:[[1,"flex-row","task-drag-handler"],[1,"flex-row","task-check"],["nz-checkbox","",3,"nzChecked","nzIndeterminate","nzCheckedChange"],[1,"flex-row","task-arrow"],[1,"flex-row","task-name"],[1,"flex-row","task-description"],[1,"flex-row","task-labels"],[1,"flex-row","task-phase","d-flex","justify-content-between","align-items-center"],["nz-tooltip","",3,"nzTooltipTitle"],["nz-button","",3,"nzShape","nzSize","nzType","click",4,"ngIf"],[1,"flex-row","task-status"],[1,"flex-row","task-priority"],[1,"flex-row","task-estimation","justify-content-center"],["nz-button","",3,"nzShape","nzSize","nzType","click"],["nz-icon","",3,"nzType","nzTheme"]],template:function(t,n){1&t&&(e._UZ(0,"div",0),e.TgZ(1,"div",1)(2,"span",2),e.NdJ("nzCheckedChange",function(r){return n.checked=r})("nzCheckedChange",function(r){return n.onAllChecked(r)}),e.qZA()(),e._UZ(3,"div",3),e.TgZ(4,"div",4),e._uU(5,"Task"),e.qZA(),e.ynx(6),e.TgZ(7,"div",5),e._uU(8,"Description"),e.qZA(),e.BQk(),e.ynx(9),e.TgZ(10,"div",6),e._uU(11,"Labels"),e.qZA(),e.BQk(),e.ynx(12),e.TgZ(13,"div",7)(14,"span",8),e._uU(15),e.ALo(16,"ellipsis"),e.qZA(),e.YNc(17,Sn,2,5,"button",9),e.qZA(),e.BQk(),e.ynx(18),e.TgZ(19,"div",10),e._uU(20,"Status"),e.qZA(),e.BQk(),e.ynx(21),e.TgZ(22,"div",11),e._uU(23,"Priority"),e.qZA(),e.BQk(),e.ynx(24),e.TgZ(25,"div",12),e._uU(26,"Estimation"),e.qZA(),e.BQk()),2&t&&(e.xp6(2),e.Q6J("nzChecked",n.checked)("nzIndeterminate",n.indeterminate),e.xp6(12),e.Q6J("nzTooltipTitle",n.phaseLabel),e.xp6(1),e.Oqu(e.xi3(16,5,n.phaseLabel,10)),e.xp6(2),e.Q6J("ngIf",n.auth.isOwnerOrAdmin()))},dependencies:[h.O5,M.Ls,O.ix,v.w,L.dQ,J.SY,we.Ie,Le.p],styles:[".flex-row[_ngcontent-%COMP%]{padding:4px 11px;background-color:#fafafa;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0;border-right:1px solid #f0f0f0;display:flex;align-items:center;flex-direction:row}.task-drag-handler[_ngcontent-%COMP%]{padding:0 0 0 4px!important;width:24px;border-bottom:1px solid #f0f0f0;border-right:none!important;position:sticky;left:0;z-index:1}.task-check[_ngcontent-%COMP%]{text-align:center;padding:8px 6px 8px 0!important;position:sticky;left:24px;z-index:1}.task-arrow[_ngcontent-%COMP%]{width:24px;padding:0!important;display:flex;align-items:center;position:sticky;border-right:0;left:47px;z-index:1}.task-name[_ngcontent-%COMP%]{width:450px;min-width:450px;position:sticky;left:71px;z-index:1}.task-name[_ngcontent-%COMP%] nz-filter-trigger[_ngcontent-%COMP%]{margin-left:auto}.task-name.left-0[_ngcontent-%COMP%]{left:47px}.task-description[_ngcontent-%COMP%]{width:225px}.task-labels[_ngcontent-%COMP%]{width:220px}.task-status[_ngcontent-%COMP%]{width:120px}.task-phase[_ngcontent-%COMP%]{width:150px}.task-priority[_ngcontent-%COMP%], .task-estimation[_ngcontent-%COMP%]{width:120px}.task-start-date[_ngcontent-%COMP%], .task-due-date[_ngcontent-%COMP%]{width:150px}"],changeDetection:0}),a})();var W=c(32181),Be=c(42753),He=c(6192);const Mn=["descriptionInput"],On=["descriptionEditor"];function An(i,a){if(1&i&&e._UZ(0,"span",8),2&i){const s=e.oxw();e.Q6J("innerHTML",s.task.description,e.oJD)}}function In(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",9)(1,"nz-form-item",10)(2,"nz-form-control",11)(3,"editor",12,13),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.task.description=n)})("onBlur",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.submit())}),e.qZA()()()()}if(2&i){const s=e.oxw();e.xp6(1),e.Udp("width","550px"),e.xp6(2),e.ekj("editing",s.isEditing),e.Q6J("init",s.CONFIG)("apiKey",s.apiKey)("ngModel",s.task.description)}}let Zn=(()=>{var i;class a{constructor(t,n,o,r){this.cdr=t,this.socket=n,this.service=o,this.ngZone=r,this.task={},this.cls="flex-row task-description p-0",this.show=!1,this.loading=!1,this.apiKey="4nquevykvy1i0q0v62ksxuu3nz1muy8i5fsqpj3wp9qm2mgp",this.CONFIG={plugins:"lists link code wordcount",toolbar:"blocks bold italic underline strikethrough | checklist numlist bullist link | alignleft aligncenter alignright alignjustify",menubar:!1,content_css:"/assets/css/prebuilt-editor.css",statusbar:!0,branding:!1,height:200,min_height:100},this.isEditing=!1,this.handleResponse=l=>{this.task.id===l?.id&&(this.task.description=l.description,this.closeDropdown(),this.cdr.markForCheck())}}ngOnInit(){this.socket.on(k.C.PT_TASK_DESCRIPTION_CHANGE.toString(),this.handleResponse)}ngOnDestroy(){this.socket.removeListener(k.C.PT_TASK_DESCRIPTION_CHANGE.toString(),this.handleResponse)}handleVisibleChange(t,n){this.show=t,t?n.classList.add(this.service.HIGHLIGHT_COL_CLS):n.classList.remove(this.service.HIGHLIGHT_COL_CLS)}submit(){this.socket.emit(k.C.PT_TASK_DESCRIPTION_CHANGE.toString(),JSON.stringify({task_id:this.task.id,description:this.task.description}))}closeDropdown(){this.ngZone.runOutsideAngular(()=>{document.body.click()})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(q.s),e.Y36(B),e.Y36(e.R0b))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-description"]],viewQuery:function(t,n){if(1&t&&(e.Gf(Mn,5),e.Gf(On,5)),2&t){let o;e.iGM(o=e.CRH())&&(n.descriptionInput=o.first),e.iGM(o=e.CRH())&&(n.descriptionEditor=o.first)}},hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:13,vars:11,consts:[["nz-dropdown","",1,"editable","w-100","h-100","d-flex","align-items-center","ps-2","pe-0","label-tag-container",3,"nzOverlayClassName","nzTrigger","nzClickHide","nzDropdownMenu","nzVisibleChange"],["element",""],["nz-tooltip","",3,"innerHTML","nzTooltipTitle"],["descriptionTooltip",""],["descriptionDropdown","nzDropdownMenu"],["class","bg-white px-0 py-0",4,"ngIf"],[1,"bg-white","d-flex","justify-content-end","p-2","border-top"],["nz-button","","nzType","primary","nzSize","small",3,"click"],["nz-typography","",1,"text-white",3,"innerHTML"],[1,"bg-white","px-0","py-0"],["nz-row","",1,"task-description-editor-list","mb-0"],[1,"description-hover","position-relative"],[1,"description-editor-list",3,"init","apiKey","ngModel","ngModelChange","onBlur"],["descriptionEditor",""]],template:function(t,n){if(1&t){const o=e.EpF();e.TgZ(0,"div",0,1),e.NdJ("nzVisibleChange",function(l){e.CHM(o);const d=e.MAs(1);return e.KtG(n.handleVisibleChange(l,d))}),e.ynx(2),e._UZ(3,"span",2),e.ALo(4,"nzEllipsis"),e.BQk(),e.qZA(),e.YNc(5,An,1,1,"ng-template",null,3,e.W1O),e.TgZ(7,"nz-dropdown-menu",null,4),e.YNc(9,In,5,7,"div",5),e.TgZ(10,"div",6)(11,"button",7),e.NdJ("click",function(){return n.submit()}),e._uU(12,"OK"),e.qZA()()()}if(2&t){const o=e.MAs(6),r=e.MAs(8);e.Q6J("nzOverlayClassName","custom-shadow")("nzTrigger","click")("nzClickHide",!1)("nzDropdownMenu",r),e.xp6(3),e.Q6J("innerHTML",e.Dn7(4,7,n.task.description,55,"..."),e.oJD)("nzTooltipTitle",o),e.xp6(6),e.Q6J("ngIf",n.show)}},dependencies:[h.O5,p.JJ,Z.t3,Z.SK,x.Nx,x.Fd,O.ix,v.w,L.dQ,Q.ZU,J.SY,p.On,E.cm,E.RR,Be.PG,He.N7],styles:["[_nghost-%COMP%]{display:block;max-width:640px}.description-editor-placeholder[_ngcontent-%COMP%]{pointer-events:none;-webkit-user-select:none;user-select:none;position:absolute;inset:0;display:flex;align-items:center}.description-editor-preview[_ngcontent-%COMP%]{position:relative;min-height:32px}.description-editor-preview.empty[_ngcontent-%COMP%]{display:flex;align-items:center;padding:10px}.task-description-editor[_ngcontent-%COMP%]{padding-left:12px;padding-right:12px;padding-bottom:12px;margin-left:-12px;margin-right:-12px;margin-bottom:-12px}"],changeDetection:0}),a})();var Ln=c(86211);let Fn=(()=>{var i;class a{transform(t,n){return t&&t.length>n?t:""}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275pipe=e.Yjl({name:"ellipsisTooltipTitlePT",type:i,pure:!0}),a})(),Nn=(()=>{var i;class a{transform(t,...n){return!(!t.end||!t.names)}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275pipe=e.Yjl({name:"endNameCheckPT",type:i,pure:!0}),a})();const En=["labelsSearchInput"];function Un(i,a){if(1&i&&(e.TgZ(0,"nz-tag",14),e.ALo(1,"withAlpha"),e.ALo(2,"safeString"),e.TgZ(3,"span"),e._uU(4),e.ALo(5,"ellipsis"),e.qZA()()),2&i){const s=e.oxw().$implicit;e.Q6J("nzColor",e.lcZ(1,3,s.color_code))("nzTooltipTitle",e.lcZ(2,5,s.names)),e.xp6(4),e.Oqu(e.xi3(5,7,s.name,10))}}function Jn(i,a){if(1&i&&(e.TgZ(0,"nz-tag",15),e.ALo(1,"ellipsisTooltipTitlePT"),e.ALo(2,"withAlpha"),e.TgZ(3,"span"),e._uU(4),e.ALo(5,"ellipsis"),e.qZA()()),2&i){const s=e.oxw().$implicit;e.Q6J("nzTooltipTitle",e.xi3(1,3,s.name,5))("nzColor",e.lcZ(2,6,s.color_code)),e.xp6(4),e.Oqu(e.xi3(5,8,s.name,10))}}function Gn(i,a){if(1&i&&(e.ynx(0)(1,11),e.ALo(2,"endNameCheckPT"),e.YNc(3,Un,6,10,"nz-tag",12),e.YNc(4,Jn,6,11,"nz-tag",13),e.BQk()()),2&i){const s=a.$implicit;e.xp6(1),e.Q6J("ngSwitch",e.lcZ(2,3,s)),e.xp6(2),e.Q6J("ngSwitchCase",!0),e.xp6(1),e.Q6J("ngSwitchCase",!1)}}function Dn(i,a){1&i&&(e.TgZ(0,"span",20),e._uU(1," Hit enter to create! "),e.qZA())}function Yn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",16)(1,"input",17,18),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.searchText=n)})("keydown.enter",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.createLabel())}),e.qZA(),e.YNc(3,Dn,2,0,"span",19),e.qZA()}if(2&i){const s=e.oxw();e.ekj("border-bottom",s.hasFilteredLabel),e.xp6(1),e.Q6J("ngModel",s.searchText),e.xp6(2),e.Q6J("ngIf",!s.hasFilteredLabel)}}function Qn(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",23),e.NdJ("nzCheckedChange",function(n){const r=e.CHM(s).$implicit;return e.KtG(r.selected=n)})("nzCheckedChange",function(){const o=e.CHM(s).$implicit,r=e.oxw(2);return e.KtG(r.handleLabelChange(o))}),e._UZ(1,"nz-badge",24),e.qZA()}if(2&i){const s=a.$implicit;e.Q6J("nzChecked",s.selected),e.xp6(1),e.Q6J("nzColor",s.color_code)("nzText",(null==s?null:s.name)||null)}}function Rn(i,a){if(1&i&&(e.TgZ(0,"ul",21),e.YNc(1,Qn,2,3,"li",22),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngForOf",s.filteredLabels)("ngForTrackBy",s.trackById)}}let Bn=(()=>{var i;class a{get hasFilteredLabel(){return!!this.filteredLabels.length}get filteredLabels(){return this.searchPipe.transform(this.labels,this.searchText)}constructor(t,n,o,r,l,d,_){this.cdr=t,this.searchPipe=n,this.auth=o,this.socket=r,this.utils=l,this.ngZone=d,this.service=_,this.task={},this.cls="flex-row task-labels",this.alpha=j.Yj,this.searchText=null,this.labels=[],this.show=!1,this.handleLabelsChange=y=>{if(y&&y.id===this.task.id){if(this.task.labels=y.labels,this.task.all_labels=y.all_labels,y.new_label)if(y.is_new){const S=[...this.service.labels];S.push(y.new_label),this.service.labels=[...S]}else{const S=this.labels.find(be=>be.id===y.new_label.id);S&&(S.selected=!0)}this.cdr.markForCheck()}},this.service.onLabelsChange$.pipe((0,G.sL)()).subscribe(()=>{this.updateLabels(),this.cdr.markForCheck()})}ngOnInit(){this.updateLabels(),this.socket.on(k.C.PT_TASK_LABELS_CHANGE.toString(),this.handleLabelsChange),this.socket.on(k.C.PT_CREATE_LABEL.toString(),this.handleLabelsChange)}ngOnDestroy(){this.labels=[],this.socket.removeListener(k.C.PT_TASK_LABELS_CHANGE.toString(),this.handleLabelsChange),this.socket.removeListener(k.C.PT_CREATE_LABEL.toString(),this.handleLabelsChange)}updateLabels(){this.labels=this.service.labels}trackById(t,n){return n.id}sortBySelected(t){this.utils.sortBySelection(t)}handleLabelsVisibleChange(t,n){if(this.show=t,t?n.classList.add(this.service.HIGHLIGHT_COL_CLS):n.classList.remove(this.service.HIGHLIGHT_COL_CLS),t){const o=this.task.all_labels?.map(r=>r.id)??[];for(const r of this.labels)r.selected=o.includes(r.id);this.focusLabelsSearchInput()}else{this.searchText=null;for(const o of this.labels)o.selected=!1}this.sortBySelected(this.labels)}focusLabelsSearchInput(){this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{this.labelsSearchInput?.nativeElement?.focus()},100)})}handleLabelChange(t){this.socket.emit(k.C.PT_TASK_LABELS_CHANGE.toString(),JSON.stringify({task_id:this.task.id,label_id:t.id,parent_task:this.task.parent_task_id})),this.sortBySelected(this.labels)}createLabel(){if(this.hasFilteredLabel||!this.searchText)return;const t=this.auth.getCurrentSession();this.socket.emit(k.C.PT_CREATE_LABEL.toString(),JSON.stringify({task_id:this.task.id,label:this.searchText.trim(),team_id:t?.team_id,parent_task:this.task.parent_task_id})),this.searchText=null,this.cdr.detectChanges()}closeDropdown(){this.ngZone.runOutsideAngular(()=>{document.body.click()})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(Oe.g),e.Y36(w.e),e.Y36(q.s),e.Y36(de.F),e.Y36(e.R0b),e.Y36(B))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-labels"]],viewQuery:function(t,n){if(1&t&&e.Gf(En,5),2&t){let o;e.iGM(o=e.CRH())&&(n.labelsSearchInput=o.first)}},hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:13,vars:11,consts:[["nz-dropdown","",1,"editable","w-100","h-100","d-flex","align-items-center","ps-2","pe-0","label-tag-container",3,"nzOverlayClassName","nzTrigger","nzClickHide","nzDropdownMenu","nzVisibleChange"],["element",""],[4,"ngFor","ngForOf","ngForTrackBy"],[1,"text-dark","avatar-dashed","empty-label","task-list-label"],["nz-typography","",3,"nzType"],["nz-icon","",3,"nzType","nzTheme"],["labelsDropdown","nzDropdownMenu"],["class","bg-white px-3 py-2",3,"border-bottom",4,"ngIf"],["style","box-shadow: none;","class","dropdown-ul","nz-menu","",4,"ngIf"],[1,"bg-white","d-flex","justify-content-end","p-2","border-top"],["nz-button","","nzType","primary","nzSize","small",3,"click"],[3,"ngSwitch"],["nz-tooltip","","class","text-dark task-list-label",3,"nzColor","nzTooltipTitle",4,"ngSwitchCase"],["class","text-dark task-list-label","nz-tooltip","",3,"nzTooltipTitle","nzColor",4,"ngSwitchCase"],["nz-tooltip","",1,"text-dark","task-list-label",3,"nzColor","nzTooltipTitle"],["nz-tooltip","",1,"text-dark","task-list-label",3,"nzTooltipTitle","nzColor"],[1,"bg-white","px-3","py-2"],["type","text","placeholder","Search or create","nz-input","",3,"ngModel","ngModelChange","keydown.enter"],["labelsSearchInput",""],["nz-typography","","nzType","secondary",4,"ngIf"],["nz-typography","","nzType","secondary"],["nz-menu","",1,"dropdown-ul",2,"box-shadow","none"],["nz-checkbox","","nz-menu-item","","class","m-0",3,"nzChecked","nzCheckedChange",4,"ngFor","ngForOf","ngForTrackBy"],["nz-checkbox","","nz-menu-item","",1,"m-0",3,"nzChecked","nzCheckedChange"],[3,"nzColor","nzText"]],template:function(t,n){if(1&t){const o=e.EpF();e.TgZ(0,"div",0,1),e.NdJ("nzVisibleChange",function(l){e.CHM(o);const d=e.MAs(1);return e.KtG(n.handleLabelsVisibleChange(l,d))}),e.YNc(2,Gn,5,5,"ng-container",2),e.TgZ(3,"nz-tag",3)(4,"span",4),e._UZ(5,"span",5),e.qZA()()(),e.TgZ(6,"nz-dropdown-menu",null,6),e.YNc(8,Yn,4,4,"div",7),e.YNc(9,Rn,2,2,"ul",8),e.TgZ(10,"div",9)(11,"button",10),e.NdJ("click",function(){return n.closeDropdown()}),e._uU(12,"OK"),e.qZA()()()}if(2&t){const o=e.MAs(7);e.Q6J("nzOverlayClassName","custom-shadow")("nzTrigger","click")("nzClickHide",!1)("nzDropdownMenu",o),e.xp6(2),e.Q6J("ngForOf",n.task.labels)("ngForTrackBy",n.trackById),e.xp6(2),e.Q6J("nzType","secondary"),e.xp6(1),e.Q6J("nzType","plus")("nzTheme","outline"),e.xp6(3),e.Q6J("ngIf",n.show),e.xp6(1),e.Q6J("ngIf",n.show)}},dependencies:[h.sg,h.O5,h.RF,h.n9,M.Ls,p.Fj,p.JJ,P.Zp,O.ix,v.w,L.dQ,Q.ZU,T.wO,T.r9,J.SY,p.On,ie.x7,E.cm,E.RR,ze.j,we.Ie,_e.m,Le.p,Ln.M,Fn,Nn],styles:[".dropdown-ul[_ngcontent-%COMP%]{max-height:250px;overflow:hidden;overflow-y:auto}.label-tag-container[_ngcontent-%COMP%]{max-width:220px;overflow:hidden;flex-wrap:wrap;padding-top:8px;padding-bottom:8px;padding-right:0}nz-tag[_ngcontent-%COMP%]{display:block;overflow:hidden;white-space:break-spaces;margin-left:3px;margin-right:3px;line-height:16px;padding-left:3px;padding-right:3px}.empty-label[_ngcontent-%COMP%]{padding:2px 8px 3px}.empty-label[_ngcontent-%COMP%] .ant-typography[_ngcontent-%COMP%]{display:flex}"],changeDetection:0}),a})(),Hn=(()=>{var i;class a{transform(t,n=0){return t?t.length>n?`${t.slice(0,n)}...`:t:""}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275pipe=e.Yjl({name:"truncateIfLongPT",type:i,pure:!0}),a})();function $n(i,a){if(1&i&&(e.TgZ(0,"nz-option",3),e.ALo(1,"truncateIfLongPT"),e._UZ(2,"nz-badge",4),e.ALo(3,"safeString"),e.qZA()),2&i){const s=a.$implicit;e.Q6J("nzValue",s.id)("nzLabel",e.xi3(1,4,s.name,12)),e.xp6(2),e.Q6J("nzColor",e.lcZ(3,7,s.color_code))("nzText",(null==s?null:s.name)||null)}}let jn=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.service=t,this.socket=n,this.cdr=o,this.ngZone=r,this.element=l,this.renderer=d,this.task={},this.cls="flex-row task-phase",this.PHASE_COLOR="#a9a9a9"+j.Yj,this.PLACEHOLDER_COLOR="rgba(0, 0, 0, 0.85) !important",this.phases=[],this.loading=!1,this.handleResponse=_=>{_&&_.task_id===this.task.id&&(this.task.phase_color=_.color_code||void 0,this.task.phase_id=_.id,this.isGroupByPhase()&&(this.task.is_sub_task||this.service.updateTaskGroup(this.task,!1),this.service.isSubtasksIncluded&&this.service.emitRefreshSubtasksIncluded()),this.cdr.markForCheck())},this.service.onPhaseChange$.pipe((0,G.sL)()).subscribe(()=>{this.updatePhases(),this.cdr.markForCheck()}),this.service.onGroupChange$.pipe((0,W.h)(_=>_.taskId===this.task.id),(0,W.h)(()=>this.isGroupByPhase()),(0,G.sL)()).subscribe(_=>{"Unmapped"===_.groupId&&(_.color=""),this.task.phase_id=_.groupId,this.task.phase_color=_.color,this.cdr.markForCheck()})}ngOnInit(){this.updatePhases(),this.socket.on(k.C.PT_TASK_PHASE_CHANGE.toString(),this.handleResponse)}ngOnDestroy(){this.socket.removeListener(k.C.PT_TASK_PHASE_CHANGE.toString(),this.handleResponse)}isGroupByPhase(){return this.service.getCurrentGroup().value===this.service.GROUP_BY_PHASE_VALUE}trackById(t,n){return n.id}handleChange(t,n){n&&this.socket.emit(k.C.PT_TASK_PHASE_CHANGE.toString(),JSON.stringify({task_id:n,phase_id:t}))}updatePhases(){this.phases=this.service.phases}toggleHighlightCls(t,n){this.ngZone.runOutsideAngular(()=>{t?this.renderer.addClass(n,this.service.HIGHLIGHT_COL_CLS):this.renderer.removeClass(n,this.service.HIGHLIGHT_COL_CLS)})}handleOpen(t){this.toggleHighlightCls(t,this.element.nativeElement)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(B),e.Y36(q.s),e.Y36(e.sBO),e.Y36(e.R0b),e.Y36(e.SBq),e.Y36(e.Qsj))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-phase"]],hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:3,vars:9,consts:[[1,"py-0"],["nzAllowClear","",1,"rounded-pill","custom-select",3,"ngModel","nzLoading","nzDropdownMatchSelectWidth","nzSize","nzPlaceHolder","ngModelChange","nzOpenChange"],["nzCustomContent","",3,"nzValue","nzLabel",4,"ngFor","ngForOf","ngForTrackBy"],["nzCustomContent","",3,"nzValue","nzLabel"],[3,"nzColor","nzText"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"nz-select",1),e.NdJ("ngModelChange",function(r){return n.task.phase_id=r})("ngModelChange",function(r){return n.handleChange(r,n.task.id)})("nzOpenChange",function(r){return n.handleOpen(r)}),e.YNc(2,$n,4,9,"nz-option",2),e.qZA()()),2&t&&(e.xp6(1),e.Udp("background-color",n.task.phase_color),e.Q6J("ngModel",n.task.phase_id)("nzLoading",n.loading)("nzDropdownMatchSelectWidth",!1)("nzSize","small")("nzPlaceHolder","Select"),e.xp6(1),e.Q6J("ngForOf",n.phases)("ngForTrackBy",n.trackById))},dependencies:[h.sg,p.JJ,K.Ip,K.Vq,p.On,ie.x7,_e.m,Hn],changeDetection:0}),a})();function qn(i,a){if(1&i&&(e.TgZ(0,"nz-option",3),e.ALo(1,"safeString"),e._UZ(2,"nz-badge",4),e.ALo(3,"safeString"),e.qZA()),2&i){const s=a.$implicit;e.Q6J("nzValue",s.id)("nzLabel",e.lcZ(1,4,s.name)),e.xp6(2),e.Q6J("nzColor",e.lcZ(3,6,s.color_code))("nzText",(null==s?null:s.name)||null)}}let Vn=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.service=t,this.socket=n,this.cdr=o,this.ngZone=r,this.element=l,this.renderer=d,this.task={},this.cls="flex-row task-status",this.statuses=[],this.loading=!1,this.handleResponse=_=>{_&&_.id===this.task.id&&(this.task.status_color=_.color_code,this.task.status=_.status_id,this.task.status_category=_.statusCategory,this.isGroupByStatus()&&(this.task.is_sub_task||this.service.updateTaskGroup(this.task,!1),this.service.isSubtasksIncluded&&this.service.emitRefreshSubtasksIncluded()),this.cdr.markForCheck())},this.service.onStatusesChange$.pipe((0,G.sL)()).subscribe(()=>{this.updateStatuses(),this.cdr.markForCheck()}),this.service.onGroupChange$.pipe((0,W.h)(_=>_.taskId===this.task.id),(0,W.h)(()=>this.isGroupByStatus()),(0,G.sL)()).subscribe(_=>{this.task.status=_.groupId,this.task.status_color=_.color,this.cdr.markForCheck()})}ngOnInit(){this.updateStatuses(),this.socket.on(k.C.PT_TASK_STATUS_CHANGE.toString(),this.handleResponse)}ngOnDestroy(){this.socket.removeListener(k.C.PT_TASK_STATUS_CHANGE.toString(),this.handleResponse)}isGroupByStatus(){return this.service.getCurrentGroup().value===this.service.GROUP_BY_STATUS_VALUE}trackById(t,n){return n.id}handleStatusChange(t,n){n&&this.socket.emit(k.C.PT_TASK_STATUS_CHANGE.toString(),JSON.stringify({task_id:n,status_id:t,parent_task:this.task.parent_task_id}))}updateStatuses(){this.statuses=this.service.statuses}toggleHighlightCls(t,n){this.ngZone.runOutsideAngular(()=>{t?this.renderer.addClass(n,this.service.HIGHLIGHT_COL_CLS):this.renderer.removeClass(n,this.service.HIGHLIGHT_COL_CLS)})}handleOpen(t){this.toggleHighlightCls(t,this.element.nativeElement)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(B),e.Y36(q.s),e.Y36(e.sBO),e.Y36(e.R0b),e.Y36(e.SBq),e.Y36(e.Qsj))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-status"]],hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:3,vars:8,consts:[[1,"py-0"],[1,"rounded-pill","custom-select",3,"ngModel","nzLoading","nzDropdownMatchSelectWidth","nzSize","ngModelChange","nzOpenChange"],["nzCustomContent","",3,"nzValue","nzLabel",4,"ngFor","ngForOf","ngForTrackBy"],["nzCustomContent","",3,"nzValue","nzLabel"],[3,"nzColor","nzText"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"nz-select",1),e.NdJ("ngModelChange",function(r){return n.task.status=r})("ngModelChange",function(r){return n.handleStatusChange(r,n.task.id)})("nzOpenChange",function(r){return n.handleOpen(r)}),e.YNc(2,qn,4,8,"nz-option",2),e.qZA()()),2&t&&(e.xp6(1),e.Udp("background-color",n.task.status_color),e.Q6J("ngModel",n.task.status)("nzLoading",n.loading)("nzDropdownMatchSelectWidth",!1)("nzSize","small"),e.xp6(1),e.Q6J("ngForOf",n.statuses)("ngForTrackBy",n.trackById))},dependencies:[h.sg,p.JJ,K.Ip,K.Vq,p.On,ie.x7,_e.m],styles:["nz-select[_ngcontent-%COMP%]{max-width:100px}"],changeDetection:0}),a})();var $e=c(8660);function Kn(i,a){if(1&i&&(e.TgZ(0,"nz-option",3),e._UZ(1,"worklenz-task-priority-label",4),e.qZA()),2&i){const s=a.$implicit;e.Q6J("nzValue",s.id)("nzLabel",s.name)("nzCustomContent",!0),e.xp6(1),e.Q6J("name",s.name)}}let Wn=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.service=t,this.socket=n,this.cdr=o,this.ngZone=r,this.element=l,this.renderer=d,this.task={},this.cls="flex-row task-priority",this.priorities=[],this.loading=!1,this.handleResponse=_=>{_&&_.id===this.task.id&&(this.task.priority_color=_.color_code,this.task.priority=_.priority_id,this.isGroupByPriority()&&(this.task.is_sub_task||this.service.updateTaskGroup(this.task,!1),this.service.isSubtasksIncluded&&this.service.emitRefreshSubtasksIncluded()),this.cdr.markForCheck())},this.service.onPrioritiesChange$.pipe((0,G.sL)()).subscribe(()=>{this.updatePriorities(),this.cdr.markForCheck()}),this.service.onGroupChange$.pipe((0,W.h)(_=>_.taskId===this.task.id),(0,W.h)(()=>this.isGroupByPriority()),(0,G.sL)()).subscribe(_=>{this.task.priority=_.groupId,this.task.priority_color=_.color,this.cdr.markForCheck()})}ngOnInit(){this.updatePriorities(),this.socket.on(k.C.PT_TASK_PRIORITY_CHANGE.toString(),this.handleResponse)}ngOnDestroy(){this.priorities=[],this.socket.removeListener(k.C.PT_TASK_PRIORITY_CHANGE.toString(),this.handleResponse)}isGroupByPriority(){return this.service.getCurrentGroup().value===this.service.GROUP_BY_PRIORITY_VALUE}trackById(t,n){return n.id}handlePriorityChange(t,n){this.socket.emit(k.C.PT_TASK_PRIORITY_CHANGE.toString(),JSON.stringify({task_id:n.id,priority_id:t,parent_task:this.task.parent_task_id}))}updatePriorities(){this.priorities=this.service.priorities}toggleHighlightCls(t,n){this.ngZone.runOutsideAngular(()=>{t?this.renderer.addClass(n,this.service.HIGHLIGHT_COL_CLS):this.renderer.removeClass(n,this.service.HIGHLIGHT_COL_CLS)})}handleOpen(t){this.toggleHighlightCls(t,this.element.nativeElement)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(B),e.Y36(q.s),e.Y36(e.sBO),e.Y36(e.R0b),e.Y36(e.SBq),e.Y36(e.Qsj))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-priority"]],hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:3,vars:8,consts:[[1,"py-0"],[1,"rounded-pill","custom-select",3,"ngModel","nzLoading","nzDropdownMatchSelectWidth","nzSize","ngModelChange","nzOpenChange"],[3,"nzValue","nzLabel","nzCustomContent",4,"ngFor","ngForOf","ngForTrackBy"],[3,"nzValue","nzLabel","nzCustomContent"],[3,"name"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"nz-select",1),e.NdJ("ngModelChange",function(r){return n.task.priority=r})("ngModelChange",function(r){return n.handlePriorityChange(r,n.task)})("nzOpenChange",function(r){return n.handleOpen(r)}),e.YNc(2,Kn,2,4,"nz-option",2),e.qZA()()),2&t&&(e.xp6(1),e.Udp("background",n.task.priority_color),e.Q6J("ngModel",n.task.priority)("nzLoading",n.loading)("nzDropdownMatchSelectWidth",!1)("nzSize","small"),e.xp6(1),e.Q6J("ngForOf",n.priorities)("ngForTrackBy",n.trackById))},dependencies:[h.sg,p.JJ,K.Ip,K.Vq,p.On,$e.o],changeDetection:0}),a})();var je=c(68373);const Xn=["labelsSearchInput"];function ei(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",7)(1,"nz-form-item",8)(2,"nz-form-control")(3,"div",9)(4,"nz-form-control",10)(5,"small",11),e._uU(6,"Hours"),e.qZA(),e.TgZ(7,"nz-input-number",12),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.task.total_hours=n)}),e.qZA()(),e.TgZ(8,"nz-form-control")(9,"small",11),e._uU(10,"Minutes"),e.qZA(),e.TgZ(11,"nz-input-number",13),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.task.total_minutes=n)}),e.qZA()()()()()()}if(2&i){const s=e.oxw();e.xp6(7),e.Q6J("ngModel",s.task.total_hours)("nzFormatter",s.utils.toRound)("nzMin",0)("nzPlaceHolder","Hours")("nzStep",1),e.xp6(4),e.Q6J("ngModel",s.task.total_minutes)("nzFormatter",s.utils.toRound)("nzMax",60)("nzMin",0)("nzPlaceHolder","Minutes")("nzStep",1)}}let ti=(()=>{var i;class a{constructor(t,n,o,r,l){this.cdr=t,this.socket=n,this.service=o,this.utils=r,this.ngZone=l,this.task={},this.cls="flex-row task-estimation p-0",this.show=!1,this.handleResponse=d=>{this.task.id===d?.id&&(this.task.total_time_string=d.total_time_string,this.closeDropdown(),this.cdr.markForCheck())}}ngOnInit(){this.socket.on(k.C.PT_TASK_TIME_ESTIMATION_CHANGE.toString(),this.handleResponse)}ngOnDestroy(){this.socket.removeListener(k.C.PT_TASK_TIME_ESTIMATION_CHANGE.toString(),this.handleResponse)}handleLabelsVisibleChange(t,n){this.show=t,t?n.classList.add(this.service.HIGHLIGHT_COL_CLS):n.classList.remove(this.service.HIGHLIGHT_COL_CLS)}submit(){this.task?.id&&this.socket.emit(k.C.PT_TASK_TIME_ESTIMATION_CHANGE.toString(),JSON.stringify({task_id:this.task.id,total_hours:this.task.total_hours||0,total_minutes:this.task.total_minutes||0,parent_task:this.task.parent_task_id}))}closeDropdown(){this.ngZone.runOutsideAngular(()=>{document.body.click()})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(q.s),e.Y36(B),e.Y36(de.F),e.Y36(e.R0b))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-task-estimation"]],viewQuery:function(t,n){if(1&t&&e.Gf(Xn,5),2&t){let o;e.iGM(o=e.CRH())&&(n.labelsSearchInput=o.first)}},hostVars:2,hostBindings:function(t,n){2&t&&e.Tol(n.cls)},inputs:{task:"task"},decls:11,vars:6,consts:[["nz-dropdown","",1,"editable","w-100","h-100","d-flex","align-items-center","justify-content-center",3,"nzOverlayClassName","nzTrigger","nzClickHide","nzDropdownMenu","nzVisibleChange"],["element",""],["nz-typography","",1,"mb-0"],["estimationDropdown","nzDropdownMenu"],["class","bg-white pt-3 px-3",4,"ngIf"],[1,"bg-white","d-flex","justify-content-end","p-2","border-top"],["nz-button","","nzType","primary","nzSize","small",3,"click"],[1,"bg-white","pt-3","px-3"],["nz-row","",1,"w-100","mb-0"],[1,"d-inline-flex",2,"position","relative","top","-12px"],[1,"me-3"],["nz-typography","","nzType","secondary",1,"d-block"],[3,"ngModel","nzFormatter","nzMin","nzPlaceHolder","nzStep","ngModelChange"],[3,"ngModel","nzFormatter","nzMax","nzMin","nzPlaceHolder","nzStep","ngModelChange"]],template:function(t,n){if(1&t){const o=e.EpF();e.TgZ(0,"div",0,1),e.NdJ("nzVisibleChange",function(l){e.CHM(o);const d=e.MAs(1);return e.KtG(n.handleLabelsVisibleChange(l,d))}),e.ynx(2),e.TgZ(3,"p",2),e._uU(4),e.qZA(),e.BQk(),e.qZA(),e.TgZ(5,"nz-dropdown-menu",null,3),e.YNc(7,ei,12,11,"div",4),e.TgZ(8,"div",5)(9,"button",6),e.NdJ("click",function(){return n.submit()}),e._uU(10,"OK"),e.qZA()()()}if(2&t){const o=e.MAs(6);e.Q6J("nzOverlayClassName","custom-shadow")("nzTrigger","click")("nzClickHide",!1)("nzDropdownMenu",o),e.xp6(4),e.Oqu(n.task.total_time_string),e.xp6(3),e.Q6J("ngIf",n.show)}},dependencies:[h.O5,p.JJ,Z.t3,Z.SK,x.Nx,x.Fd,O.ix,v.w,L.dQ,Q.ZU,p.On,E.cm,E.RR,je._V],changeDetection:0}),a})(),ni=(()=>{var i;class a{transform(t,...n){return t.sub_tasks_count?"#191919":"rgba(0, 0, 0, 0.45)"}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275pipe=e.Yjl({name:"subTasksArrowColorPT",type:i,pure:!0}),a})(),ii=(()=>{var i;class a{transform(t,...n){return t?"down":"right"}}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275pipe=e.Yjl({name:"subTasksArrowIconPT",type:i,pure:!0}),a})();function si(i,a){1&i&&e._UZ(0,"div",15)}function oi(i,a){1&i&&(e.TgZ(0,"div",16),e._UZ(1,"span",17),e.qZA()),2&i&&(e.xp6(1),e.Q6J("nzType","holder")("nzTheme","outline"))}function ai(i,a){if(1&i&&(e._UZ(0,"span",24),e.ALo(1,"subTasksArrowIconPT")),2&i){const s=e.oxw(3);e.Q6J("nzType",e.lcZ(1,2,s.task.show_sub_tasks))("nzTheme","outline")}}function ri(i,a){1&i&&e._UZ(0,"span",25),2&i&&e.Q6J("nzType","loading")}function li(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",20),e.NdJ("click",function(n){e.CHM(s);const o=e.oxw(2);return n.stopPropagation(),e.KtG(o.openSubTasks())}),e.ynx(1),e.TgZ(2,"span",21),e.ALo(3,"subTasksArrowColorPT"),e.YNc(4,ai,2,4,"span",22),e.YNc(5,ri,1,1,"span",23),e.qZA(),e.BQk(),e.qZA()}if(2&i){const s=e.oxw(2);e.xp6(2),e.Udp("color",e.lcZ(3,6,s.task)),e.ekj("hidden-arrow",!s.Number(s.task.sub_tasks_count)&&!s.task.show_sub_tasks),e.xp6(2),e.Q6J("ngIf",!s.task.sub_tasks_loading),e.xp6(1),e.Q6J("ngIf",s.task.sub_tasks_loading)}}function ci(i,a){if(1&i&&(e.TgZ(0,"div",18),e.YNc(1,li,6,8,"div",19),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("ngIf",!s.task.is_sub_task)}}function pi(i,a){1&i&&(e.TgZ(0,"small",32),e._UZ(1,"span",17),e.qZA()),2&i&&(e.Q6J("nzType","secondary"),e.xp6(1),e.Q6J("nzType","double-right")("nzTheme","outline"))}function di(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",30),e.NdJ("click",function(n){e.CHM(s);const o=e.oxw(2),r=e.MAs(10);return e.KtG(o.onTaskNameClick(n,r,o.task))}),e.ALo(1,"safeString"),e.YNc(2,pi,2,3,"small",31),e._uU(3),e.qZA()}if(2&i){const s=e.oxw(2);e.Q6J("nzTooltipMouseEnterDelay",.5)("nzTooltipTitle",e.lcZ(1,4,s.task.name)),e.xp6(2),e.Q6J("ngIf",s.task.is_sub_task),e.xp6(1),e.hij(" ",s.task.name," \xa0 ")}}function ui(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"nz-tag",33),e.NdJ("click",function(n){e.CHM(s);const o=e.oxw(2);return n.stopPropagation(),e.KtG(o.openSubTasks())}),e.TgZ(1,"span",34),e._uU(2),e._UZ(3,"span",35),e.qZA()()}if(2&i){const s=e.oxw(2);e.Q6J("nzColor","default"),e.xp6(2),e.hij(" ",s.task.sub_tasks_count," "),e.xp6(1),e.Q6J("nzType","double-right")("nzTheme","outline")}}function _i(i,a){if(1&i&&(e.TgZ(0,"div",26)(1,"div",27)(2,"div"),e.YNc(3,di,4,6,"div",28),e.qZA(),e.YNc(4,ui,4,4,"nz-tag",29),e.qZA()()),2&i){const s=e.oxw();e.xp6(3),e.Q6J("ngIf",s.editId!==s.task.id),e.xp6(1),e.Q6J("ngIf",!s.task.is_sub_task&&!s.service.isSubtasksIncluded)}}function mi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"input",36),e.NdJ("focus",function(){e.CHM(s);const n=e.oxw(),o=e.MAs(7),r=e.MAs(10);return n.selectCol(o),e.KtG(n.selectCol(r))})("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.task.name=n)})("blur",function(){e.CHM(s);const n=e.oxw(),o=e.MAs(7),r=e.MAs(10);return n.handleNameChange(n.task),n.deselectCol(o),e.KtG(n.deselectCol(r))})("keydown.enter",function(){e.CHM(s);const n=e.oxw(),o=e.MAs(7),r=e.MAs(10);return n.handleNameChange(n.task),n.deselectCol(o),e.KtG(n.deselectCol(r))}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("ngModel",s.task.name)("nzBorderless",!0)}}let hi=(()=>{var i;class a{get id(){return this.task.id}constructor(t,n,o,r,l,d,_,y){this.element=t,this.renderer=n,this.cdr=o,this.service=r,this.socket=l,this.map=d,this.ngZone=_,this.utils=y,this.cls="position-relative task-row",this.onShowSubTasks=new e.vpe,this.highlight="highlight-col",this.Number=Number,this.editId=null,this.selected=!1,this.handleNameChangeResponse=S=>{S&&this.id===S.id&&this.task&&this.task.name!=S.name&&(this.task.name=S.name,this.markForCheck())},this.handleEstimationChangeResponse=S=>{S.id===this.id&&(this.task.total_time_string=S.total_time_string,this.cdr.markForCheck())},this.service.onColumnsChange$.pipe((0,G.sL)()).subscribe(()=>{this.markForCheck()}),(0,Ze.T)(this.map.onSelect$.pipe((0,W.h)(S=>S.id===this.id),(0,W.h)(()=>!this.selected)),this.map.onDeselect$.pipe((0,W.h)(S=>S.id===this.id),(0,W.h)(()=>this.selected)),this.map.onDeselectAll$.pipe((0,W.h)(()=>this.selected))).pipe((0,G.sL)()).subscribe(S=>{this.selected=!this.selected,this.toggleSelection(),this.markForCheck()})}toggleSelection(){this.ngZone.runOutsideAngular(()=>{const t="selected",n=this.element.nativeElement;this.selected?this.renderer.addClass(n,t):this.renderer.removeClass(n,t)})}ngOnInit(){this.registerSocketEvents()}ngOnDestroy(){this.unregisterSocketEvents()}registerSocketEvents(){this.socket.on(k.C.PT_TASK_NAME_CHANGE.toString(),this.handleNameChangeResponse),this.socket.on(k.C.PT_TASK_TIME_ESTIMATION_CHANGE.toString(),this.handleEstimationChangeResponse)}unregisterSocketEvents(){this.socket.removeListener(k.C.PT_TASK_NAME_CHANGE.toString(),this.handleNameChangeResponse),this.socket.removeListener(k.C.PT_TASK_TIME_ESTIMATION_CHANGE.toString(),this.handleEstimationChangeResponse)}onContextMenu(t){this.service.emitOnContextMenu(t,this.task)}focus(t){setTimeout(()=>{t.querySelector("input")?.focus()})}onCheckChange(t){t?this.map.selectTask(this.task):this.map.deselectTask(this.task),this.toggleSelection()}openSubTasks(){this.onShowSubTasks?.emit(this.task)}selectCol(t){t.classList.contains(this.highlight)||t.classList.add(this.highlight)}deselectCol(t){t.classList.remove(this.highlight),this.editId=null}handleNameChange(t){t&&(this.socket.emit(k.C.PT_TASK_NAME_CHANGE.toString(),JSON.stringify({task_id:t.id,name:t.name,parent_task:this.task.parent_task_id})),this.editId=null)}onTaskNameClick(t,n,o){t.stopPropagation(),this.focus(n),this.editId=o.id||null}markForCheck(){this.cdr.markForCheck()}detectChanges(){this.cdr.detectChanges()}onDragStart(){this.map.deselectAll(),this.detectChanges()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.SBq),e.Y36(e.Qsj),e.Y36(e.sBO),e.Y36(B),e.Y36(q.s),e.Y36(ue),e.Y36(e.R0b),e.Y36(de.F))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-pt-task-list-row"]],hostVars:2,hostBindings:function(t,n){1&t&&e.NdJ("contextmenu",function(r){return n.onContextMenu(r)}),2&t&&e.Tol(n.cls)},inputs:{task:"task"},outputs:{onShowSubTasks:"onShowSubTasks"},decls:26,vars:16,consts:[["cdkDrag","",1,"flex-table","rows",3,"ngClass","cdkDragData","cdkDragLockAxis","cdkDragBoundary","cdkDragStarted"],["class","task-placeholder",4,"cdkDragPlaceholder"],["cdkDragHandle","",1,"flex-row","task-drag-handler",3,"cdkDragHandleDisabled"],["class","drag-handle",4,"ngIf"],[1,"flex-row","task-check"],["nz-checkbox","",1,"p-0",3,"nzChecked","nzCheckedChange"],[1,"flex-row","task-arrow"],["tr0",""],["class","p-0 border-end-0",4,"ngIf"],[1,"flex-row","task-name"],["tr1",""],[1,"inner-task-name-container"],["class","editable-cell pointer-text ps-1 w-100",4,"ngIf"],["nz-input","","class","ps-1 name-input","style","min-width: 365px; max-width: 365px;",3,"ngModel","nzBorderless","focus","ngModelChange","blur","keydown.enter",4,"ngIf"],[3,"task"],[1,"task-placeholder"],[1,"drag-handle"],["nz-icon","",3,"nzType","nzTheme"],[1,"p-0","border-end-0"],["class","d-flex align-items-center justify-content-center sub-tasks-arrow dropdown-highlight",3,"click",4,"ngIf"],[1,"d-flex","align-items-center","justify-content-center","sub-tasks-arrow","dropdown-highlight",3,"click"],[1,"align-items-center","align-self-center","cursor-pointer","d-flex","sub-arrow"],["style","font-size: 12px","nz-icon","",3,"nzType","nzTheme",4,"ngIf"],["nz-icon","",3,"nzType",4,"ngIf"],["nz-icon","",2,"font-size","12px",3,"nzType","nzTheme"],["nz-icon","",3,"nzType"],[1,"editable-cell","pointer-text","ps-1","w-100"],[1,"d-flex","w-100"],["nz-tooltip","","class","task-name-text",3,"nzTooltipMouseEnterDelay","nzTooltipTitle","click",4,"ngIf"],["class","me-1 px-1 double-arrow",3,"nzColor","click",4,"ngIf"],["nz-tooltip","",1,"task-name-text",3,"nzTooltipMouseEnterDelay","nzTooltipTitle","click"],["nz-typography","",3,"nzType",4,"ngIf"],["nz-typography","",3,"nzType"],[1,"me-1","px-1","double-arrow",3,"nzColor","click"],[2,"color","#6d6e6f"],["nz-icon","",2,"font-size","10px",3,"nzType","nzTheme"],["nz-input","",1,"ps-1","name-input",2,"min-width","365px","max-width","365px",3,"ngModel","nzBorderless","focus","ngModelChange","blur","keydown.enter"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0),e.NdJ("cdkDragStarted",function(){return n.onDragStart()}),e.YNc(1,si,1,0,"div",1),e.TgZ(2,"div",2),e.YNc(3,oi,2,2,"div",3),e.qZA(),e.TgZ(4,"div",4)(5,"span",5),e.NdJ("nzCheckedChange",function(r){return n.selected=r})("nzCheckedChange",function(r){return n.onCheckChange(r)}),e.qZA()(),e.TgZ(6,"div",6,7),e.YNc(8,ci,2,1,"div",8),e.qZA(),e.TgZ(9,"div",9,10)(11,"div",11),e.YNc(12,_i,5,2,"div",12),e.qZA(),e.YNc(13,mi,1,2,"input",13),e.qZA(),e.ynx(14),e._UZ(15,"worklenz-task-description",14),e.BQk(),e.ynx(16),e._UZ(17,"worklenz-task-labels",14),e.BQk(),e.ynx(18),e._UZ(19,"worklenz-task-phase",14),e.BQk(),e.ynx(20),e._UZ(21,"worklenz-task-status",14),e.BQk(),e.ynx(22),e._UZ(23,"worklenz-task-priority",14),e.BQk(),e.ynx(24),e._UZ(25,"worklenz-task-estimation",14),e.BQk(),e.qZA()),2&t&&(e.Q6J("ngClass",n.task.is_sub_task?"subtask":"main-task")("cdkDragData",n.task)("cdkDragLockAxis","y")("cdkDragBoundary",".tasks-wrapper"),e.xp6(2),e.Q6J("cdkDragHandleDisabled",!!n.task.parent_task_id),e.xp6(1),e.Q6J("ngIf",!n.task.parent_task_id),e.xp6(2),e.Q6J("nzChecked",n.selected),e.xp6(3),e.Q6J("ngIf",!n.service.isSubtasksIncluded),e.xp6(4),e.Q6J("ngIf",n.editId!==n.task.id),e.xp6(1),e.Q6J("ngIf",n.editId===n.task.id),e.xp6(2),e.Q6J("task",n.task),e.xp6(2),e.Q6J("task",n.task),e.xp6(2),e.Q6J("task",n.task),e.xp6(2),e.Q6J("task",n.task),e.xp6(2),e.Q6J("task",n.task),e.xp6(2),e.Q6J("task",n.task))},dependencies:[h.mk,h.O5,M.Ls,p.Fj,p.JJ,P.Zp,v.w,Q.ZU,J.SY,p.On,ze.j,we.Ie,Ce.Zt,Ce.Bh,Zn,Bn,jn,Vn,Wn,ti,_e.m,ni,ii],styles:["[_nghost-%COMP%]{position:relative;display:table-row;vertical-align:inherit;border-color:inherit;-webkit-user-select:none;user-select:none}[_nghost-%COMP%]:hover .plus-icon[_ngcontent-%COMP%]{display:block}[_nghost-%COMP%]:hover td[_ngcontent-%COMP%]{background:#ecf0f3}[_nghost-%COMP%]:hover .hidden-arrow[_ngcontent-%COMP%]{display:flex!important}.hidden-arrow[_ngcontent-%COMP%]{display:none!important}.dropdown-highlight[_ngcontent-%COMP%]:hover{background-color:#d0eefa54;border:#5587f5 1px solid;border-radius:3px}.plus-icon[_ngcontent-%COMP%]{display:none;position:absolute;right:0;z-index:1;top:0;bottom:0;height:100%}.expanded[_ngcontent-%COMP%]{transform:rotate(-90deg)}.sub-tasks-arrow[_ngcontent-%COMP%]{position:relative;cursor:pointer;left:3px;width:16px;padding:2px;border:1px solid transparent;z-index:1}.sub-tasks-arrow[_ngcontent-%COMP%] .sub-arrow[_ngcontent-%COMP%]{width:10px;height:10px;color:#191919;margin-left:-2px}.task-name-text[_ngcontent-%COMP%]{border:1px solid transparent;padding-left:2px;border-radius:4px}.task-name-text[_ngcontent-%COMP%]:hover{border:1px solid #d9d9d9}.task-name[_ngcontent-%COMP%]{border:1px solid transparent}.task-name[_ngcontent-%COMP%]:hover{cursor:text;background:#fff;border-radius:4px}.highlight-col[_ngcontent-%COMP%]{border:1px solid #1890ff!important}.highlight-col[_ngcontent-%COMP%] nz-date-picker[_ngcontent-%COMP%]{box-shadow:none}.editable[_ngcontent-%COMP%] .add-button[_ngcontent-%COMP%]{visibility:hidden}.editable[_ngcontent-%COMP%]:hover .add-button[_ngcontent-%COMP%]{visibility:visible}.ant-popover[_ngcontent-%COMP%]{width:500px}.flex-table[_ngcontent-%COMP%]{display:flex}.rows[_ngcontent-%COMP%] .flex-row[_ngcontent-%COMP%]{padding:3px 12px;border-bottom:1px solid #f0f0f0;border-right:1px solid #f0f0f0;background:white;display:flex;align-items:center;max-height:50px;border-radius:0}.rows[_ngcontent-%COMP%]:hover .flex-row[_ngcontent-%COMP%]{background:#f8f7f9}.subtask[_ngcontent-%COMP%] .flex-row[_ngcontent-%COMP%]{background:#fcfcfc}.task-check[_ngcontent-%COMP%]{text-align:center;padding:8px 6px 8px 0!important;border-left:none;position:sticky;left:24px;z-index:1}.task-arrow[_ngcontent-%COMP%]{width:24px;min-width:24px;padding:8px 11px 8px 2px!important;border-right:none!important;position:sticky;left:47px;z-index:1}.task-arrow.highlight-col[_ngcontent-%COMP%]{border-top:1px solid #188fff!important;border-left:1px solid #188fff!important;border-bottom:1px solid #188fff!important}.task-name[_ngcontent-%COMP%]{width:450px;min-width:450px;position:sticky;left:71px;z-index:1;border-radius:0;padding-right:65px!important}.task-name.highlight-col[_ngcontent-%COMP%]{border-top:1px solid #188fff!important;border-right:1px solid #188fff!important;border-bottom:1px solid #188fff!important;border-left:none!important}.task-name.left-0[_ngcontent-%COMP%]{left:47px}.task-key[_ngcontent-%COMP%]{width:85px;min-width:85px;padding-left:4px!important;padding-right:4px!important;justify-content:center}.task-key[_ngcontent-%COMP%] nz-tag[_ngcontent-%COMP%]{padding-left:4px;padding-right:4px;max-width:80px;text-overflow:ellipsis;overflow:hidden}.task-description[_ngcontent-%COMP%]{width:225px;min-width:225px;overflow:hidden;display:grid!important}.task-progress[_ngcontent-%COMP%]{width:80px;min-width:80px}.task-labels[_ngcontent-%COMP%]{padding:0!important}.task-labels[_ngcontent-%COMP%] .editable[_ngcontent-%COMP%]{padding:6px 11px;align-items:center;display:flex}.task-members[_ngcontent-%COMP%]{padding:0!important}.task-members[_ngcontent-%COMP%] .editable[_ngcontent-%COMP%]{padding:6px 11px;align-items:center;display:flex}.task-members[_ngcontent-%COMP%]{width:160px;min-width:160px}.task-labels[_ngcontent-%COMP%]{width:220px;min-width:220px}.task-status[_ngcontent-%COMP%]{width:120px;min-width:120px}.task-phase[_ngcontent-%COMP%]{width:150px;min-width:150px}.task-priority[_ngcontent-%COMP%], .task-time-tracking[_ngcontent-%COMP%], .task-estimation[_ngcontent-%COMP%]{width:120px;min-width:120px}.task-start-date[_ngcontent-%COMP%], .task-due-date[_ngcontent-%COMP%], .task-completed-date[_ngcontent-%COMP%], .task-created-date[_ngcontent-%COMP%], .task-update-date[_ngcontent-%COMP%]{width:150px;min-width:150px}.task-due-date[_ngcontent-%COMP%]{padding:0!important}.task-due-date[_ngcontent-%COMP%] .editable[_ngcontent-%COMP%]{align-items:center;display:flex}.task-drag-handler[_ngcontent-%COMP%]{padding:0 0 0 4px!important;width:24px;min-width:24px;border-right:none!important;position:sticky;left:0;z-index:1;background-color:#fff}.drag-handle[_ngcontent-%COMP%]{cursor:grab;opacity:.8}.drag-handle[_ngcontent-%COMP%]:hover span[_ngcontent-%COMP%]{color:#1890ff}.drag-handle[_ngcontent-%COMP%]:active{cursor:grabbing}.task-name-text[_ngcontent-%COMP%]{width:100%;-webkit-line-clamp:1;display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.inner-icon-cont[_ngcontent-%COMP%]{width:max-content;display:flex;justify-content:flex-end;align-items:center;column-gap:4px}.name-input[_ngcontent-%COMP%]{padding:5px 12px;border-left:1px solid transparent}.double-arrow[_ngcontent-%COMP%]{line-height:16px;border:none;cursor:pointer}.task-placeholder[_ngcontent-%COMP%]{width:100%;height:42px;border:1px dashed #d9d9d9;background:#fafafa}.v-line[_ngcontent-%COMP%]{background-color:#188fff!important;position:absolute;inset:0 -5px 0 0;width:1px;margin:auto}.double-arrow[_ngcontent-%COMP%]{height:16px;margin-top:4px}"]}),a})();var gi=c(59773);const fi=["contextMenuDropdown"];function zi(i,a){if(1&i&&e._uU(0),2&i){const s=e.oxw().$implicit;e.hij(" ",(null==s?null:s.name)||null," ")}}function Ti(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",9),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw(2);return e.KtG(r.changeGroup(o.id))}),e._UZ(1,"nz-badge",10),e.ALo(2,"safeString"),e.YNc(3,zi,1,1,"ng-template",null,11,e.W1O),e.qZA()}if(2&i){const s=a.$implicit,t=e.MAs(4);e.xp6(1),e.Q6J("nzColor",e.lcZ(2,2,s.color_code))("nzText",t)}}function Ci(i,a){if(1&i&&(e.TgZ(0,"li",7)(1,"ul"),e.YNc(2,Ti,5,4,"li",8),e.qZA()()),2&i){const s=e.oxw(),t=e.MAs(9);e.Q6J("nzTitle",t),e.xp6(2),e.Q6J("ngForOf",s.groups)}}function ki(i,a){1&i&&(e._UZ(0,"span",12),e._uU(1," Move to\n"))}let xi=(()=>{var i;class a{constructor(t,n,o,r,l,d){this.contextMenuService=t,this.service=n,this.map=o,this.api=r,this.socket=l,this.cdr=d,this.templateId=null,this.groups=[],this.deleting=!1,this.hasSubTasks=!1,this.selectedTask=null,this.destroy$=new R.x,this.service.onContextMenu$.pipe((0,gi.R)(this.destroy$)).subscribe(_=>{this.onContextMenu(_)})}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}onContextMenu(t){this.selectedTask=t.task,this.map.deselectAll(),this.map.selectTask(t.task),this.hasSubTasks=this.isSelectionHasSubTasks(),this.cdr.detectChanges(),this.contextMenuService.create(t.event,this.contextMenuDropdown)}isSelectionHasSubTasks(){return this.map.getSelectedTasks().some(t=>t.is_sub_task)}changeGroup(t){if(!this.selectedTask)return;const n=this.service.getCurrentGroup();n.value===this.service.GROUP_BY_STATUS_VALUE?this.handleStatusChange(t,this.selectedTask.id):n.value===this.service.GROUP_BY_PRIORITY_VALUE?this.handlePriorityChange(t,this.selectedTask.id):n.value===this.service.GROUP_BY_PHASE_VALUE&&this.handlePhaseChange(t,this.selectedTask.id)}handleStatusChange(t,n){n&&this.socket.emit(k.C.PT_TASK_STATUS_CHANGE.toString(),JSON.stringify({task_id:n,status_id:t}))}handlePriorityChange(t,n){n&&this.socket.emit(k.C.PT_TASK_PRIORITY_CHANGE.toString(),JSON.stringify({task_id:n,priority_id:t}))}handlePhaseChange(t,n){n&&this.socket.emit(k.C.PT_TASK_PHASE_CHANGE.toString(),{task_id:n,phase_id:t})}delete(){var t=this;return(0,m.Z)(function*(){if(!t.deleting)try{t.deleting=!0;const n=t.map.getSelectedTaskIds(),o=yield t.api.bulkDelete({tasks:n},t.templateId);if(o.done)for(const r of o.body.deleted_tasks)t.service.deleteTask(r);t.deleting=!1}catch{t.deleting=!1}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(E.Iw),e.Y36(B),e.Y36(ue),e.Y36(Ye),e.Y36(q.s),e.Y36(e.sBO))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-context-menu"]],viewQuery:function(t,n){if(1&t&&e.Gf(fi,5),2&t){let o;e.iGM(o=e.CRH())&&(n.contextMenuDropdown=o.first)}},inputs:{templateId:"templateId",groups:"groups"},decls:10,vars:4,consts:[["contextMenuDropdown","nzDropdownMenu"],["nz-menu",""],[3,"ngSwitch"],["nz-submenu","",3,"nzTitle",4,"ngSwitchCase"],["nz-menu-item","",3,"click"],["nz-icon","",1,"me-2",3,"nzType","nzTheme"],["titleTemplate",""],["nz-submenu","",3,"nzTitle"],["class","m-0","nz-menu-item","",3,"click",4,"ngFor","ngForOf"],["nz-menu-item","",1,"m-0",3,"click"],[3,"nzColor","nzText"],["textTmpl",""],["nz-icon","","nzType","retweet","nzTheme","outline",1,"me-2"]],template:function(t,n){1&t&&(e.TgZ(0,"nz-dropdown-menu",null,0)(2,"ul",1),e.ynx(3,2),e.YNc(4,Ci,3,2,"li",3),e.BQk(),e.TgZ(5,"li",4),e.NdJ("click",function(){return n.delete()}),e._UZ(6,"span",5),e._uU(7," Delete "),e.qZA()()(),e.YNc(8,ki,2,0,"ng-template",null,6,e.W1O)),2&t&&(e.xp6(3),e.Q6J("ngSwitch",n.hasSubTasks),e.xp6(1),e.Q6J("ngSwitchCase",!1),e.xp6(2),e.Q6J("nzType",n.deleting?"loading":"delete")("nzTheme","outline"))},dependencies:[h.sg,h.RF,h.n9,M.Ls,v.w,T.wO,T.r9,T.rY,ie.x7,E.RR,_e.m],changeDetection:0}),a})();var bi=c(34554),Fe=c(66987);function vi(i,a){1&i&&(e.TgZ(0,"button",20),e._uU(1," Search "),e.qZA()),2&i&&e.Q6J("nzSize","small")("nzType","primary")}function wi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",21),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.reset())}),e._uU(1," Reset "),e.qZA()}2&i&&e.Q6J("nzSize","small")}function yi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",22),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw();return e.KtG(r.changeGroup(o))}),e.TgZ(1,"span",23),e._uU(2),e.qZA()()}if(2&i){const s=a.$implicit,t=e.oxw();e.Q6J("nzSelected",s.value===t.selectedGroup.value),e.xp6(2),e.Oqu(s.label)}}function Si(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",24),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.createStatusClick())}),e._uU(1," Add Status "),e.qZA()}2&i&&e.Q6J("nzType","primary")("nzTooltipTitle","Create status")}function Pi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"button",24),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.phaseSettingsClick())}),e._uU(1," Add Phase "),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("nzType","primary")("nzTooltipTitle",s.phaseLabel+" settings")}}let Mi=(()=>{var i;class a{get selectedGroup(){return this.service.getCurrentGroup()}get phaseLabel(){return this.phaseService.label}constructor(t,n,o,r,l,d,_,y){this.cdr=t,this.ngZone=n,this.tasksApi=o,this.socket=r,this.utils=l,this.phaseService=d,this.service=_,this.auth=y,this.onGroupBy=new e.vpe,this.onFilterSearch=new e.vpe,this.onPhaseSettingsClick=new e.vpe,this.onCreateStatusClick=new e.vpe,this.ASCEND="ascend",this.DESCEND="descend",this.COUNTS_LABELS_STYLE={backgroundColor:"#1890ff",color:"#fff"},this.taskSearch=null}changeGroup(t){this.service.setCurrentGroup(t),this.onGroupBy.emit(t)}isGroupByStatus(){return this.selectedGroup.value===this.service.GROUP_BY_STATUS_VALUE}isGroupByPhase(){return this.selectedGroup.value===this.service.GROUP_BY_PHASE_VALUE}trackById(t,n){return n.id}toIdsMap(t){return t.map(n=>n.id).join("+")}search(){this.taskSearch&&(this.onFilterSearch.emit(encodeURIComponent(this.taskSearch)),document.body.click())}reset(){this.taskSearch&&(this.taskSearch=null,this.onFilterSearch.emit(this.taskSearch),this.ngZone.runOutsideAngular(()=>{document.body.click()}))}onSearchDropdownVisibleChange(t){t&&this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{document.querySelector("#task-search-input")?.focus()},j.GR)})}phaseSettingsClick(){this.onPhaseSettingsClick?.emit()}createStatusClick(){this.onCreateStatusClick?.emit()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(e.sBO),e.Y36(e.R0b),e.Y36(bi.c),e.Y36(q.s),e.Y36(de.F),e.Y36(Re.u),e.Y36(B),e.Y36(w.e))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-group-filter"]],inputs:{templateId:"templateId"},outputs:{onGroupBy:"onGroupBy",onFilterSearch:"onFilterSearch",onPhaseSettingsClick:"onPhaseSettingsClick",onCreateStatusClick:"onCreateStatusClick"},decls:26,vars:22,consts:[["nz-form","",3,"nzLayout"],["nz-row","",3,"nzAlign","nzJustify"],["nz-col",""],["nz-button","","nz-dropdown","",1,"me-2",3,"nzDropdownMenu","nzTrigger","nzVisibleChange"],["nz-icon","","nzType","search","nzTheme","outline"],["taskSearchDropdown","nzDropdownMenu"],[1,"bg-white","shadow","rounded-4","p-2",3,"submit"],["type","text","nz-input","","name","search","id","task-search-input","placeholder","Search by name",3,"ngModel","ngModelChange"],[1,"mt-2"],["nz-button","","type","submit",3,"nzSize","nzType",4,"nzSpaceItem"],["nz-button","","type","button",3,"nzSize","click",4,"nzSpaceItem"],[3,"nzType"],[1,"ms-1"],["nz-button","","nz-dropdown","",1,"ms-1","me-2",3,"nzTrigger","nzDropdownMenu","nzClickHide"],["nz-tooltip","",3,"nzTooltipTitle"],["nz-icon","",3,"nzType"],["groupByMenu",""],["nz-menu",""],["class","m-0","nz-menu-item","",3,"nzSelected","click",4,"ngFor","ngForOf","ngForTrackBy"],["nz-button","","class","ms-0","nz-tooltip","",3,"nzType","nzTooltipTitle","click",4,"ngIf"],["nz-button","","type","submit",3,"nzSize","nzType"],["nz-button","","type","button",3,"nzSize","click"],["nz-menu-item","",1,"m-0",3,"nzSelected","click"],["nz-typography",""],["nz-button","","nz-tooltip","",1,"ms-0",3,"nzType","nzTooltipTitle","click"]],template:function(t,n){if(1&t&&(e.TgZ(0,"form",0)(1,"div",1)(2,"div",2)(3,"button",3),e.NdJ("nzVisibleChange",function(r){return n.onSearchDropdownVisibleChange(r)}),e._UZ(4,"span",4),e.qZA(),e.TgZ(5,"nz-dropdown-menu",null,5)(7,"form",6),e.NdJ("submit",function(){return n.search()}),e.TgZ(8,"input",7),e.NdJ("ngModelChange",function(r){return n.taskSearch=r}),e.qZA(),e.TgZ(9,"nz-space",8),e.YNc(10,vi,2,2,"button",9),e.YNc(11,wi,2,1,"button",10),e.qZA()()(),e._UZ(12,"nz-divider",11),e.TgZ(13,"label",12),e._uU(14,"Group by: "),e.qZA(),e.TgZ(15,"button",13)(16,"span",14),e._uU(17),e.ALo(18,"ellipsis"),e.qZA(),e._UZ(19,"span",15),e.qZA(),e.TgZ(20,"nz-dropdown-menu",null,16)(22,"ul",17),e.YNc(23,yi,3,2,"li",18),e.qZA()(),e.YNc(24,Si,2,2,"button",19),e.YNc(25,Pi,2,2,"button",19),e.qZA()()()),2&t){const o=e.MAs(6),r=e.MAs(21);e.Q6J("nzLayout","vertical"),e.xp6(1),e.Q6J("nzAlign","bottom")("nzJustify","space-between"),e.xp6(2),e.ekj("filter-active",!!n.taskSearch),e.Q6J("nzDropdownMenu",o)("nzTrigger","click"),e.xp6(5),e.Q6J("ngModel",n.taskSearch),e.xp6(4),e.Q6J("nzType","vertical"),e.xp6(3),e.Q6J("nzTrigger","click")("nzDropdownMenu",r)("nzClickHide",!0),e.xp6(1),e.Q6J("nzTooltipTitle",n.selectedGroup.label),e.xp6(1),e.hij(" ",e.xi3(18,19,n.selectedGroup.label,15)," "),e.xp6(2),e.Q6J("nzType","caret-down"),e.xp6(4),e.Q6J("ngForOf",n.service.GROUP_BY_OPTIONS)("ngForTrackBy",n.trackById),e.xp6(1),e.Q6J("ngIf",n.isGroupByStatus()&&n.auth.isOwnerOrAdmin()),e.xp6(1),e.Q6J("ngIf",n.isGroupByPhase()&&n.auth.isOwnerOrAdmin())}},dependencies:[h.sg,h.O5,M.Ls,p._Y,p.Fj,p.JJ,p.JL,Z.t3,Z.SK,x.Lr,P.Zp,O.ix,v.w,L.dQ,Q.ZU,T.wO,T.r9,H.NU,H.$1,J.SY,p.On,p.F,E.cm,E.RR,E.wA,Fe.g,Le.p]}),a})(),Oi=(()=>{var i;class a{get label(){return this._label||this.DEFAULT_LABEL}set label(t){this._label=t||this.DEFAULT_LABEL}get onLabelChange(){return this._labelChangeSbj$.asObservable()}get onPhaseOptionsChange(){return this._phaseOptionsChangeSbj$.asObservable()}constructor(t){this.list=t,this._labelChangeSbj$=new R.x,this._phaseOptionsChangeSbj$=new R.x,this.DEFAULT_LABEL="Phase",this._label=null}updateLabel(t){const n=this.list.GROUP_BY_OPTIONS.find(o=>o.value===this.list.GROUP_BY_PHASE_VALUE);n&&(this.label=t,n.label=this.label,this._labelChangeSbj$.next(this.label))}emitOptionsChange(){this._phaseOptionsChangeSbj$.next()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.LFG(B))},i.\u0275prov=e.Yz7({token:i,factory:i.\u0275fac,providedIn:"root"}),a})();var ke=c(43389);function Ai(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"li",23),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw().$implicit,l=e.oxw(2);return e.KtG(l.setColorCode(r,o))}),e.TgZ(1,"nz-tag",24),e._uU(2,"\xa0 "),e.qZA()()}if(2&i){const s=a.$implicit;e.xp6(1),e.Q6J("nzColor",s+"69")}}function Ii(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"nz-form-item",14)(1,"nz-form-control")(2,"input",15,16),e.NdJ("ngModelChange",function(n){const r=e.CHM(s).$implicit;return e.KtG(r.name=n)})("focus",function(){const o=e.CHM(s).$implicit,r=e.MAs(3),l=e.oxw(2);return r.select(),e.KtG(l.setNameCache(o.id,o.name))})("blur",function(){const o=e.CHM(s).$implicit,r=e.oxw(2);return e.KtG(r.updateOption(o))})("keyup.enter",function(){e.CHM(s);const n=e.MAs(3);return e.KtG(n.blur())}),e.qZA()(),e.TgZ(4,"nz-tag",17),e._uU(5,"\xa0 "),e.qZA(),e.TgZ(6,"nz-dropdown-menu",null,18)(8,"ul",19),e.YNc(9,Ai,3,1,"li",20),e.qZA()(),e.TgZ(10,"nz-form-label",21)(11,"button",22),e.NdJ("click",function(){const o=e.CHM(s).$implicit,r=e.oxw(2);return e.KtG(r.removeOption(o.id))}),e._UZ(12,"span",12),e.qZA()()()}if(2&i){const s=a.$implicit,t=a.index,n=e.MAs(7),o=e.oxw(2);e.xp6(2),e.Q6J("name","opt"+t)("ngModel",s.name)("minLength",2)("maxLength",30)("disabled",o.deleting[s.id]),e.xp6(2),e.Q6J("nzColor",s.color_code)("nzDropdownMenu",n)("nzTrigger","click"),e.xp6(5),e.Q6J("ngForOf",o.COLOR_CODES),e.xp6(1),e.Q6J("nzNoColon",!0),e.xp6(1),e.Q6J("nzShape","circle")("nzType","text")("nzLoading",o.deleting[s.id]),e.xp6(1),e.Q6J("nzType","close-circle")("nzTheme","outline")}}function Zi(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"nz-skeleton",2)(2,"form",3)(3,"nz-form-item")(4,"nz-form-label",4),e._uU(5,"Phase Label:"),e.qZA(),e.TgZ(6,"nz-form-control")(7,"input",5),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.phaseLabel=n)})("focus",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.oldLabel=n.phaseLabel)})("blur",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.updateLabel(n.phaseLabel))}),e.qZA()()()(),e._UZ(8,"nz-divider",6),e.TgZ(9,"form",7)(10,"div",8)(11,"div",9)(12,"nz-form-label"),e._uU(13," Phase Options "),e.qZA()(),e.TgZ(14,"div",10)(15,"button",11),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.addNewOption())}),e._UZ(16,"span",12),e._uU(17," Add Option "),e.qZA()()(),e.YNc(18,Ii,13,15,"nz-form-item",13),e.qZA()(),e.BQk()}if(2&i){const s=e.oxw();e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",s.loading),e.xp6(1),e.Q6J("nzLayout","vertical"),e.xp6(2),e.Q6J("nzFor","label"),e.xp6(3),e.Q6J("ngModel",s.phaseLabel),e.xp6(8),e.Q6J("nzType","primary")("nzLoading",s.creating),e.xp6(1),e.Q6J("nzType","plus")("nzTheme","outline"),e.xp6(2),e.Q6J("ngForOf",s.options)}}let Li=(()=>{var i;class a{get options(){return this.list.phases}constructor(t,n,o,r){this.api=t,this.cdr=n,this.list=o,this.service=r,this.templateId=null,this.show=!1,this.showChange=new e.vpe,this.onCreateOrUpdate=new e.vpe,this.refresh=new e.vpe,this.COLOR_CODES=j.zW,this.loading=!1,this.creating=!1,this.updatingLabel=!1,this.updating={},this.deleting={},this.updateCache={},this.oldLabel=null,this.phaseLabel=null}close(){this.show=!1,this.showChange.emit(!1)}addNewOption(){this.create()}onVisibleChange(t){t&&(this.get(!0),this.phaseLabel=this.service.label)}removeOption(t){t&&this.delete(t)}updateOption(t){var n=this;return(0,m.Z)(function*(){yield n.update(t),delete n.updateCache[t.id]})()}setNameCache(t,n){this.updateCache[t]=n}create(){var t=this;return(0,m.Z)(function*(){if(t.templateId&&!t.creating){try{t.creating=!0,(yield t.api.create(t.templateId)).done&&(yield t.get(!1),t.service.emitOptionsChange(),t.onCreateOrUpdate.emit()),t.creating=!1}catch{t.creating=!1}t.cdr.markForCheck()}})()}get(t){var n=this;return(0,m.Z)(function*(){if(n.templateId){try{n.loading=t;const o=yield n.api.get(n.templateId);o.done&&(n.list.phases=o.body),n.loading=!1}catch{n.loading=!1}n.cdr.markForCheck()}})()}updateLabel(t){var n=this;return(0,m.Z)(function*(){if(n.templateId){if(!t?.trim())return void(n.phaseLabel=n.oldLabel);try{n.updatingLabel=!0,(yield n.api.updateLabel(n.templateId,t?.trim())).done&&n.service.updateLabel(t?.trim()),n.updatingLabel=!1}catch{n.updatingLabel=!1}n.cdr.markForCheck()}})()}update(t){var n=this;return(0,m.Z)(function*(){if(t?.id&&n.templateId&&!n.updating[t.id]&&n.updateCache[t.id]!==t.name){try{n.updating[t.id]=!0,(yield n.api.update(n.templateId,t)).done&&(yield n.get(!1),n.service.emitOptionsChange(),n.onCreateOrUpdate.emit()),n.updating[t.id]=!1}catch{t.name=n.updateCache[t.id],n.updating[t.id]=!1}n.cdr.markForCheck()}})()}delete(t){var n=this;return(0,m.Z)(function*(){if(t&&n.templateId&&!n.deleting[t]){try{if(n.deleting[t]=!0,(yield n.api.delete(t,n.templateId)).done){const r=n.list.phases.findIndex(l=>l.id===t);r>-1&&(n.list.phases.splice(r,1),n.service.emitOptionsChange(),n.onCreateOrUpdate.emit())}n.deleting[t]=!1}catch{n.deleting[t]=!1}n.cdr.markForCheck()}})()}setColorCode(t,n){var o=this;return(0,m.Z)(function*(){t.color_code=n+"69",yield o.updateColor(t)})()}updateColor(t){var n=this;return(0,m.Z)(function*(){if(t?.id&&n.templateId){try{n.updating[t.id]=!0,(yield n.api.updateColor(n.templateId,t)).done&&n.refresh.emit(),n.updating[t.id]=!1}catch{t.name=n.updateCache[t.id],n.updating[t.id]=!1}n.cdr.markForCheck()}})()}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(Ie),e.Y36(e.sBO),e.Y36(B),e.Y36(Oi))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-phase-settings-drawer"]],inputs:{templateId:"templateId",show:"show"},outputs:{showChange:"showChange",onCreateOrUpdate:"onCreateOrUpdate",refresh:"refresh"},decls:2,vars:4,consts:[[3,"nzClosable","nzVisible","nzPlacement","nzTitle","nzOnClose","nzVisibleChange"],[4,"nzDrawerContent"],[3,"nzActive","nzLoading"],["nz-form","",3,"nzLayout"],[3,"nzFor"],["nz-input","","id","label","placeholder","Enter a name for label","name","label","required","",3,"ngModel","ngModelChange","focus","blur"],[1,"mb-3"],["nz-form",""],[1,"d-flex","align-items-center","justify-content-between","mb-3"],[1,"d-block"],[1,"d-block","ms-auto"],["type","button","nz-button","","nzBlock","",3,"nzType","nzLoading","click"],["nz-icon","",3,"nzType","nzTheme"],["class","d-flex align-items-center mb-3",4,"ngFor","ngForOf"],[1,"d-flex","align-items-center","mb-3"],["nz-input","",3,"name","ngModel","minLength","maxLength","disabled","ngModelChange","focus","blur","keyup.enter"],["input",""],["nz-dropdown","",1,"ms-2","rounded-circle","cursor-pointer",2,"width","20px","height","20px",3,"nzColor","nzDropdownMenu","nzTrigger"],["menu","nzDropdownMenu"],["nz-menu","","nzSelectable","",2,"max-height","200px","overflow","hidden","overflow-y","auto"],["nz-menu-item","",3,"click",4,"ngFor","ngForOf"],[3,"nzNoColon"],["type","button","nz-button","",3,"nzShape","nzType","nzLoading","click"],["nz-menu-item","",3,"click"],[1,"me-1","w-100","rounded-pill",2,"height","16px !important","width","16px !important",3,"nzColor"]],template:function(t,n){1&t&&(e.TgZ(0,"nz-drawer",0),e.NdJ("nzOnClose",function(){return n.close()})("nzVisibleChange",function(r){return n.onVisibleChange(r)}),e.YNc(1,Zi,19,10,"ng-container",1),e.qZA()),2&t&&e.Q6J("nzClosable",!0)("nzVisible",n.show)("nzPlacement","right")("nzTitle","Configure Phases")},dependencies:[h.sg,M.Ls,p._Y,p.Fj,p.JJ,p.JL,p.Q7,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,P.Zp,O.ix,v.w,L.dQ,T.wO,T.r9,$.ng,p.On,p.F,E.cm,E.RR,ze.j,ke.Vz,ke.SQ,Fe.g],changeDetection:0}),a})();function Fi(i,a){if(1&i&&(e.TgZ(0,"nz-option",11),e._UZ(1,"nz-badge",12),e.qZA()),2&i){const s=a.$implicit;e.Q6J("nzLabel",s.name||null)("nzValue",s.id),e.xp6(1),e.Q6J("nzTooltipTitle",s.description)("nzText",(null==s?null:s.name)||null)("nzTooltipPlacement","left")("nzColor",s.color_code)}}function Ni(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"nz-skeleton",2)(2,"form",3),e.NdJ("submit",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.submit())}),e.TgZ(3,"nz-form-item")(4,"nz-form-label",4),e._uU(5,"Name"),e.qZA(),e.TgZ(6,"nz-form-control",5),e._UZ(7,"input",6),e.qZA()(),e.TgZ(8,"nz-form-item")(9,"nz-form-label",4),e._uU(10,"Category"),e.qZA(),e.TgZ(11,"nz-form-control",7)(12,"nz-select",8),e.YNc(13,Fi,2,6,"nz-option",9),e.qZA()()(),e.TgZ(14,"button",10),e._uU(15),e.qZA()()(),e.BQk()}if(2&i){const s=e.oxw();e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",s.loading),e.xp6(1),e.Q6J("formGroup",s.form)("nzLayout","vertical"),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(1),e.Q6J("formControlName","name"),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(1),e.Q6J("formControlName","category_id"),e.xp6(1),e.Q6J("ngForOf",s.categories),e.xp6(2),e.Oqu(s.action)}}let Ei=(()=>{var i;class a{constructor(t,n,o){this.api=t,this.fb=n,this.app=o,this.action="Create",this.show=!1,this.statusId=null,this.templateId=null,this.showChange=new e.vpe,this.onCreateOrUpdate=new e.vpe,this.loading=!0,this.loadingCategories=!1,this.colorCodes=j.lD,this.categories=[],this.taskStatus={},this.createForm()}init(){this.form.controls.template_id.setValue(this.templateId),this.getCategories(),this.statusId?this.getById(this.statusId):this.loading=!1}closeModal(){this.show=!1,this.form.reset(),this.action="Create",this.createForm(),this.showChange.emit()}submit(){var t=this;return(0,m.Z)(function*(){t.taskStatus&&t.taskStatus.id?yield t.updateStatus():yield t.addStatus()})()}getById(t){var n=this;return(0,m.Z)(function*(){try{n.loading=!0;const o=yield n.api.getById(t);o.done&&(n.taskStatus=o.body,n.form.patchValue(n.taskStatus)),n.loading=!1}catch(o){(0,b.tu)(o),n.loading=!1}})()}getCategories(){var t=this;return(0,m.Z)(function*(){try{t.loadingCategories=!0;const n=yield t.api.getCategories();n.done&&(t.categories=n.body,t.form.controls.category_id.setValue(t.categories[0].id)),t.loadingCategories=!1}catch(n){t.loadingCategories=!1,(0,b.tu)(n)}})()}addStatus(){var t=this;return(0,m.Z)(function*(){if(t.form.invalid)t.app.displayErrorsOf(t.form);else try{const n=yield t.api.create(t.form.value);n.done&&(n.body.color_code=n.body.color_code+"69",t.onCreateOrUpdate.emit(),t.closeModal())}catch(n){(0,b.tu)(n)}})()}updateStatus(){var t=this;return(0,m.Z)(function*(){if(t.taskStatus&&t.taskStatus.id){if(t.form.invalid)return void t.app.displayErrorsOf(t.form);try{(yield t.api.update(t.taskStatus.id,t.form.value)).done&&(t.onCreateOrUpdate.emit(),t.closeModal(),(0,me.mT)())}catch(n){(0,b.tu)(n)}}})()}onVisibilityChange(t){t&&setTimeout(()=>this.init(),100)}createForm(){this.form=this.fb.group({name:[null,[p.kI.required]],category_id:[null,[p.kI.required]],template_id:[this.templateId]})}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(Ae),e.Y36(p.qu),e.Y36(U.z))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-status-settings-drawer"]],inputs:{action:"action",show:"show",statusId:"statusId",templateId:"templateId"},outputs:{showChange:"showChange",onCreateOrUpdate:"onCreateOrUpdate"},decls:2,vars:3,consts:[["nzPlacement","right",3,"nzVisible","nzClosable","nzTitle","nzOnClose","nzVisibleChange"],[4,"nzDrawerContent"],[3,"nzActive","nzLoading"],["nz-form","",3,"formGroup","nzLayout","submit"],["nzRequired","",3,"nzSpan"],["nzErrorTip","Please enter a name!",3,"nzSpan"],["nz-input","","placeholder","Name",3,"formControlName"],[3,"nzSpan"],[3,"formControlName"],["nzCustomContent","",3,"nzLabel","nzValue",4,"ngFor","ngForOf"],["nz-button","","nzBlock","","nzType","primary","type","submit"],["nzCustomContent","",3,"nzLabel","nzValue"],["nz-tooltip","",1,"w-100",3,"nzTooltipTitle","nzText","nzTooltipPlacement","nzColor"]],template:function(t,n){1&t&&(e.TgZ(0,"nz-drawer",0),e.NdJ("nzOnClose",function(){return n.closeModal()})("nzVisibleChange",function(r){return n.show=r})("nzVisibleChange",function(r){return n.onVisibilityChange(r)}),e.YNc(1,Ni,16,12,"ng-container",1),e.qZA()),2&t&&(e.MGl("nzTitle","",n.action," Status"),e.Q6J("nzVisible",n.show)("nzClosable",!0))},dependencies:[h.sg,p._Y,p.Fj,p.JJ,p.JL,p.sg,p.u,Z.t3,Z.SK,x.Lr,x.Nx,x.iK,x.Fd,P.Zp,O.ix,v.w,L.dQ,J.SY,$.ng,K.Ip,K.Vq,ie.x7,ke.Vz,ke.SQ]}),a})();const Ui=["input"];function Ji(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"h4",3),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.focusInput())}),e._uU(1),e.qZA()}if(2&i){const s=e.oxw();e.xp6(1),e.Oqu(s.templateName)}}function Gi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"input",4,5),e.NdJ("ngModelChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.templateName=n)})("ngModelChange",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.validate())})("blur",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.onBlur())}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("ngClass",s.isEmpty?"error":"")("ngModel",s.templateName)}}function Di(i,a){1&i&&(e.TgZ(0,"small",6),e._uU(1,"Name cannot be empty!"),e.qZA())}let Yi=(()=>{var i;class a{constructor(t,n,o,r){this.socket=t,this.ngZone=n,this.renderer=o,this.cdr=r,this.templateId=null,this.templateName=null,this.showInput=!1,this.isEmpty=!1,this.handleResponse=l=>{l&&(this.templateName=l.template_name,this.showInput=!1,this.cdr.markForCheck())}}ngOnInit(){this.socket.on(k.C.PT_NAME_CHANGE.toString(),this.handleResponse)}focusInput(){this.showInput=!0,this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{this.input&&(this.input.nativeElement.focus(),this.cdr.markForCheck())},100)})}onBlur(){this.validate()&&this.changeName()}validate(){return""!==this.templateName?.trim()&&this.templateName?(this.isEmpty=!1,!0):(this.isEmpty=!0,!1)}changeName(){this.socket.emit(k.C.PT_NAME_CHANGE.toString(),JSON.stringify({template_id:this.templateId,template_name:this.templateName}))}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(q.s),e.Y36(e.R0b),e.Y36(e.Qsj),e.Y36(e.sBO))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-template-name"]],viewQuery:function(t,n){if(1&t&&e.Gf(Ui,5),2&t){let o;e.iGM(o=e.CRH())&&(n.input=o.first)}},inputs:{templateId:"templateId",templateName:"templateName"},decls:3,vars:3,consts:[["nz-typography","","class","mb-0",3,"click",4,"ngIf"],["nz-input","","placeholder","Template Name","type","text","class","temp-name-input",3,"ngClass","ngModel","ngModelChange","blur",4,"ngIf"],["nz-typography","","nzType","danger","class","error-text",4,"ngIf"],["nz-typography","",1,"mb-0",3,"click"],["nz-input","","placeholder","Template Name","type","text",1,"temp-name-input",3,"ngClass","ngModel","ngModelChange","blur"],["input",""],["nz-typography","","nzType","danger",1,"error-text"]],template:function(t,n){1&t&&(e.YNc(0,Ji,2,1,"h4",0),e.YNc(1,Gi,2,2,"input",1),e.YNc(2,Di,2,0,"small",2)),2&t&&(e.Q6J("ngIf",!n.showInput),e.xp6(1),e.Q6J("ngIf",n.showInput),e.xp6(1),e.Q6J("ngIf",n.isEmpty))},dependencies:[h.mk,h.O5,p.Fj,p.JJ,P.Zp,Q.ZU,p.On],styles:[".temp-name-input[_ngcontent-%COMP%]{color:#000000d9;font-weight:600;font-size:18px;line-height:1.4}.error[_ngcontent-%COMP%]{border-color:#ff4d4f;box-shadow:none}.error-text[_ngcontent-%COMP%]{position:absolute;left:0;bottom:-16px}h4[_ngcontent-%COMP%]{transition:.25s all;border:1px solid transparent}h4[_ngcontent-%COMP%]:hover{border:1px solid #d9d9d9;border-radius:4px}"],changeDetection:0}),a})();const Qi=["row"],Ri=["scrollPanel"];function Bi(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"span")(1,"button",14),e.NdJ("click",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.onBack())}),e._UZ(2,"span",15),e.qZA()()}}function Hi(i,a){if(1&i&&(e.TgZ(0,"span",16),e._UZ(1,"worklenz-template-name",17),e.qZA()),2&i){const s=e.oxw();e.xp6(1),e.Q6J("templateId",s.templateId)("templateName",s.templateName)}}function $i(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"worklenz-group-filter",18),e.NdJ("onGroupBy",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.onGroupByChange(n))})("onFilterSearch",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.handleFilterSearch(n))})("onPhaseSettingsClick",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.openAddColumnDrawer())})("onCreateStatusClick",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.openStatusDrawer())}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("templateId",s.templateId)}}function ji(i,a){if(1&i&&(e.TgZ(0,"div",19)(1,"div",20),e._UZ(2,"img",21),e.qZA(),e._UZ(3,"span",22),e.qZA()),2&i){const s=e.oxw();e.xp6(3),e.Q6J("ngSwitch",s.isGroupByPhase())}}function qi(i,a){1&i&&(e.TgZ(0,"div",39),e._uU(1," No tasks available "),e.qZA())}function Vi(i,a){1&i&&e._UZ(0,"worklenz-pt-task-list-row",44),2&i&&e.Q6J("task",a.$implicit)}function Ki(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"div",45,46)(3,"div",47),e._uU(4," \xa0 "),e.qZA(),e.TgZ(5,"div",48)(6,"worklenz-add-task-input",49,50),e.NdJ("focusChange",function(n){e.CHM(s);const o=e.MAs(2),r=e.MAs(7),l=e.oxw(4);return e.KtG(l.quickTaskFocusChange(n,o,r))}),e.qZA()()(),e.BQk()}if(2&i){const s=e.oxw(2).$implicit,t=e.oxw().$implicit,n=e.oxw();e.xp6(6),e.Q6J("templateId",n.templateId)("label","Add sub-task")("parentTask",s.id)("groupId",t.id)("subTaskInput",!0)}}function Wi(i,a){if(1&i&&(e.ynx(0),e.TgZ(1,"div"),e.YNc(2,Vi,1,1,"worklenz-pt-task-list-row",43),e.YNc(3,Ki,8,5,"ng-container",42),e.qZA(),e.BQk()),2&i){const s=e.oxw().$implicit,t=e.oxw(2);e.xp6(2),e.Q6J("ngForOf",s.sub_tasks)("ngForTrackBy",t.trackById),e.xp6(1),e.Q6J("ngIf",t.templateId&&s.id&&s.show_sub_tasks)}}function Xi(i,a){if(1&i){const s=e.EpF();e.ynx(0),e.TgZ(1,"worklenz-pt-task-list-row",40,41),e.NdJ("onShowSubTasks",function(n){e.CHM(s);const o=e.MAs(2),r=e.oxw().$implicit,l=e.oxw();return e.KtG(l.displaySubTasks(n,o,r.id))}),e.qZA(),e.YNc(3,Wi,4,3,"ng-container",42),e.BQk()}if(2&i){const s=a.$implicit,t=e.oxw(2);e.xp6(1),e.ekj("selected",(null==t.selectedTask?null:t.selectedTask.id)===s.id),e.Q6J("task",s),e.xp6(2),e.Q6J("ngIf",s.sub_tasks&&s.show_sub_tasks)}}function es(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"worklenz-add-task-input",51),e.NdJ("focusChange",function(n){e.CHM(s),e.oxw();const o=e.MAs(14),r=e.oxw();return e.KtG(r.handleFocusChange(n,o))}),e.qZA()}if(2&i){const s=e.oxw().$implicit,t=e.oxw();e.Q6J("templateId",t.templateId)("groupId",s.id)}}function ts(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"div",23)(1,"div",24)(2,"worklenz-task-list-group-settings",25),e.NdJ("toggle",function(n){e.CHM(s);const o=e.MAs(4),r=e.oxw();return e.KtG(r.toggleGroup(n,o))})("onCreateOrUpdate",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.refreshWithoutLoading())}),e.qZA(),e.TgZ(3,"div",26,27),e._UZ(5,"span",28),e.TgZ(6,"div",29)(7,"div",30,31),e.NdJ("cdkDropListDropped",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.onDrop(n))})("scroll",function(){e.CHM(s);const n=e.MAs(8),o=e.oxw();return e.KtG(o.onScroll(n))}),e.TgZ(9,"div",32)(10,"worklenz-task-list-header",33),e.NdJ("selectChange",function(n){const r=e.CHM(s).$implicit,l=e.oxw();return e.KtG(l.selectTasksInGroup(n,r))})("phaseSettingsClick",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.openAddColumnDrawer())}),e.qZA(),e.YNc(11,qi,2,0,"div",34),e.YNc(12,Xi,4,4,"ng-container",35),e.qZA()(),e.TgZ(13,"div",36,37),e.YNc(15,es,1,2,"worklenz-add-task-input",38),e.qZA()()()()()}if(2&i){const s=a.$implicit,t=e.oxw();e.xp6(2),e.Q6J("group",s)("templateId",t.templateId)("categories",t.categories),e.xp6(1),e.Udp("max-height",s.tasks.length?void 0:0),e.xp6(2),e.Udp("background",s.color_code),e.xp6(2),e.Q6J("id",s.id)("cdkDropListConnectedTo",t.groupIds)("cdkDropListData",s),e.xp6(3),e.Q6J("groupId",s.id),e.xp6(1),e.Q6J("ngIf",!s.tasks.length),e.xp6(1),e.Q6J("ngForOf",s.tasks)("ngForTrackBy",t.trackById),e.xp6(3),e.Q6J("ngIf",t.templateId)}}function ns(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"worklenz-phase-settings-drawer",52),e.NdJ("showChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.displayPhaseModal=n)})("onCreateOrUpdate",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.refreshWithoutLoading())})("refresh",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.refreshWithoutLoading())}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("templateId",s.templateId)("show",s.displayPhaseModal)}}function is(i,a){if(1&i){const s=e.EpF();e.TgZ(0,"worklenz-status-settings-drawer",53),e.NdJ("onCreateOrUpdate",function(){e.CHM(s);const n=e.oxw();return e.KtG(n.refreshWithoutLoading())})("showChange",function(n){e.CHM(s);const o=e.oxw();return e.KtG(o.displayStatusModal=n)}),e.qZA()}if(2&i){const s=e.oxw();e.Q6J("statusId",null)("templateId",s.templateId)("show",s.displayStatusModal)}}const os=[{path:"",component:ve,children:[{path:"",redirectTo:"profile",pathMatch:"full"},{path:"profile",component:F},{path:"language-and-region",component:ft},{path:"labels",canActivate:[oe.T],component:yt},{path:"categories",canActivate:[oe.T],loadChildren:()=>c.e(931).then(c.bind(c,82931)).then(i=>i.CategoriesModule)},{path:"clients",canActivate:[oe.T],loadChildren:()=>c.e(554).then(c.bind(c,9554)).then(i=>i.ClientsModule)},{path:"job-titles",canActivate:[oe.T],loadChildren:()=>c.e(693).then(c.bind(c,93693)).then(i=>i.JobTitlesModule)},{path:"notifications",loadChildren:()=>c.e(940).then(c.bind(c,94940)).then(i=>i.NotificationSettingsModule)},{path:"teams",component:at},{path:"team-members",canActivate:[oe.T],component:nn},{path:"password",canActivate:[(i,a)=>(0,e.f3M)(ut).canActivate(i,a)],component:dt},{path:"task-templates",canActivate:[oe.T],component:It},{path:"project-templates",canActivate:[oe.T],component:cn}]},{path:"project-templates/edit/:id/:name",canActivate:[oe.T],component:(()=>{var i;class a{get loading(){return this.loadingGroups}get defaultStatus(){return this.service.statuses.length?this.service.statuses[0].id:null}get groups(){return this.service.groups}constructor(t,n,o,r,l,d,_,y,S,be,ne,ae,fo,zo,To,Co){this.route=t,this.router=n,this.cdr=o,this.service=r,this.ngZone=l,this.map=d,this.socket=_,this.renderer=y,this.utils=S,this.api=be,this.labelsApi=ne,this.statusesApi=ae,this.prioritiesApi=fo,this.phasesApi=zo,this.app=To,this.auth=Co,this.scrollBy=0,this.templateId=null,this.templateName="",this.searchValue=null,this.templatesFilterString=null,this.selected=!1,this.loadingGroups=!1,this.groupChanging=!1,this.displayPhaseModal=!1,this.displayStatusModal=!1,this.checked=!1,this.indeterminate=!1,this.loadingFiltering=!1,this.loadingStatuses=!1,this.loadingPriorities=!1,this.loadingCategories=!1,this.loadingPhases=!1,this.loadingLabels=!1,this.taskId=null,this.selectedTask=null,this.groupIds=[],this.categories=[],this.handleSortOrderResponse=qe=>{for(const Ve of qe){const Ne=Ve.id;if(Ne){const Ee=this.map.tasks.get(Ne);Ee&&(Ee.sort_order=Ve.sort_order,this.map.tasks.set(Ne,Ee))}}},this.templateId=this.route.snapshot.paramMap.get("id"),this.templateName=this.route.snapshot.paramMap.get("name"),this.app.setTitle("Edit Template"),this.service.settemplateId(this.templateId),this.service.onTaskAddOrDelete$.pipe((0,G.sL)()).subscribe(qe=>{this.cdr.markForCheck()}),this.service.onRefresh$.pipe((0,G.sL)()).subscribe(()=>{this.cdr.markForCheck()}),this.service.onRefreshSubtasksIncluded.pipe((0,G.sL)()).subscribe(()=>{this.cdr.markForCheck()})}ngOnInit(){this.service.isSubtasksIncluded=!1,this.init(!0)}init(t){Promise.all([this.getGroups(t),this.getLabels(),this.getStatuses(),this.getPriorities(),this.getCategories(),this.getPhases()]),this.socket.on(k.C.PT_TASK_SORT_ORDER_CHANGE.toString(),this.handleSortOrderResponse)}ngOnDestroy(){this.ngZone.runOutsideAngular(()=>{this.service.reset(),this.service.groups=[],this.map.reset()}),this.socket.removeListener(k.C.PT_TASK_SORT_ORDER_CHANGE.toString(),this.handleSortOrderResponse)}isGroupByPhase(){return this.service.getCurrentGroup().value===this.service.GROUP_BY_PHASE_VALUE}getConf(t){const n={id:this.templateId,search:this.searchValue,projects:this.templatesFilterString,group:this.service.getCurrentGroup().value,isSubtasksInclude:!1};return t&&(n.parent_task=t),n}displaySubTasks(t,n,o){var r=this;return(0,m.Z)(function*(){if(t.id||!t.sub_tasks_loading){if(!t.show_sub_tasks&&0===t.sub_tasks_count)return t.show_sub_tasks=!0,void(t.sub_tasks=[]);if(t.sub_tasks_loading=!0,t.show_sub_tasks=!t.show_sub_tasks,t.show_sub_tasks){t.sub_tasks=yield r.getSubTasks(t);for(const l of t.sub_tasks)r.map.add(o,l)}else{for(const l of t.sub_tasks||[])r.map.deselectTask(l);t.sub_tasks=[]}t.sub_tasks_loading=!1,n.detectChanges(),r.cdr.markForCheck()}})()}toggleGroup(t,n){this.ngZone.runOutsideAngular(()=>{const o=t.target;o&&(o.closest(".btn")?.classList.toggle("active"),this.renderer.setStyle(n,"max-height","0px"===n.style.maxHeight?n.scrollHeight+8+"px":"0px"))})}trackById(t,n){return n.id}onDrop(t){const n=t.previousIndex,o=t.currentIndex,r=t.previousContainer.data,l=t.container.data;0==r.tasks.length&&this.ngZone.runOutsideAngular(()=>{document.getElementById(`${t.previousContainer.id}`)?.closest("div")?.parentNode?.parentNode?.parentNode?.querySelector("button.collapse.active")?.classList.remove("active")});const d=t.item.data,_=l.tasks[o]?.sort_order;this.socket.emit(k.C.PT_TASK_SORT_ORDER_CHANGE.toString(),{template_id:this.service.gettemplateId(),from_index:r.tasks[n].sort_order,to_index:_||l.tasks[l.tasks.length-1]?.sort_order||-1,to_last_index:!_,from_group:r.id,to_group:l.id,group_by:this.service.getCurrentGroup().value,task:d,team_id:this.auth.getCurrentSession()?.team_id}),r.id===l.id?(0,Ce.bA)(t.container.data.tasks,n,o):((0,Ce.EA)(t.previousContainer.data.tasks,t.container.data.tasks,t.previousIndex,t.currentIndex),this.map.remove(d),this.map.add(l.id,d),this.service.emitGroupChange(l.id,d.id,l.color_code))}bulkUpdateSuccess(){var t=this;return(0,m.Z)(function*(){yield t.getGroups(!0)})()}selectTasksInGroup(t,n){for(const o of n.tasks)t?this.map.selectTask(o):this.map.deselectTask(o)}mapTasks(t){for(const n of t){this.map.registerGroup(n);for(const o of n.tasks)o.start_date&&(o.start_date=new Date(o.start_date)),o.end_date&&(o.end_date=new Date(o.end_date))}}toggleFocusCls(t,n){t?this.renderer.addClass(n,this.service.HIGHLIGHT_COL_CLS):this.renderer.removeClass(n,this.service.HIGHLIGHT_COL_CLS)}onShowChange(t){t||(this.selectedTask=null)}handleNewTaskReceive(t){if(t.isSubTask){const n=this.rows.find(o=>o.id===t.taskId);n&&n.detectChanges()}this.cdr.markForCheck()}handleFocusChange(t,n){this.ngZone.runOutsideAngular(()=>{this.toggleFocusCls(t,n)})}quickTaskFocusChange(t,n,o){this.ngZone.runOutsideAngular(()=>{this.toggleFocusCls(t,n)})}onGroupByChange(t){var n=this;return(0,m.Z)(function*(){n.service.setCurrentGroup(t),n.groupChanging=!0,yield n.getGroups(!0),setTimeout(()=>{n.groupChanging=!1,n.cdr.markForCheck()},100)})()}handleFilterSearch(t){var n=this;return(0,m.Z)(function*(){n.loadingFiltering=!0,n.searchValue=t,yield n.getGroups(!0),n.loadingFiltering=!1,n.cdr.markForCheck()})()}getGroups(t){var n=this;return(0,m.Z)(function*(){if(n.templateId){try{n.map.deselectAll(),n.loadingGroups=t;const o=n.getConf();o.isSubtasksInclude=n.service.isSubtasksIncluded;const r=yield n.api.getTaskList(o);if(r.done){const l=(0,b.I8)(r.body);n.groupIds=l.map(d=>d.id),n.mapTasks(l),n.service.groups=l}n.loadingGroups=!1}catch{n.loadingGroups=!1}n.cdr.markForCheck()}})()}getSubTasks(t){var n=this;return(0,m.Z)(function*(){let o=[];if(t?.id)try{const r=n.getConf(t.id),l=yield n.api.getTaskList(r);l.done&&(o=l.body)}catch{}return o})()}getStatuses(){var t=this;return(0,m.Z)(function*(){if(t.templateId)try{t.loadingStatuses=!0;const n=yield t.statusesApi.get(t.templateId);n.done&&(t.service.statuses=n.body),t.loadingStatuses=!1}catch{t.loadingStatuses=!1}})()}getPriorities(){var t=this;return(0,m.Z)(function*(){try{t.loadingPriorities=!0;const n=yield t.prioritiesApi.get();n.done&&(t.service.priorities=n.body),t.loadingPriorities=!1}catch{t.loadingPriorities=!1}})()}getCategories(){var t=this;return(0,m.Z)(function*(){try{t.loadingCategories=!0;const n=yield t.statusesApi.getCategories();n.done&&(t.categories=n.body),t.loadingCategories=!1}catch{t.loadingCategories=!1}})()}getPhases(){var t=this;return(0,m.Z)(function*(){if(t.templateId)try{t.loadingPhases=!0;const n=yield t.phasesApi.get(t.templateId);n.done&&(t.service.phases=n.body),t.loadingPhases=!1}catch{t.loadingPhases=!1}})()}getLabels(){var t=this;return(0,m.Z)(function*(){try{t.loadingLabels=!0;const n=yield t.labelsApi.get(t.templateId);n.done&&(t.service.labels=n.body),t.loadingLabels=!1}catch{t.loadingLabels=!1}})()}onScroll(t){this.ngZone.runOutsideAngular(()=>{const n="scrolling-panel";this.scrollBy=t.scrollLeft,this.scrollBy>0?t.classList.add(n):t.classList.remove(n)})}debounce(t,n){let o;return(...r)=>{clearTimeout(o),o=setTimeout(()=>t.apply(this,r),n)}}openAddColumnDrawer(){this.displayPhaseModal=!0}openStatusDrawer(){this.displayStatusModal=!0}onBack(){this.router.navigate(["/worklenz/settings/project-templates"])}refreshWithoutLoading(){this.init(!1)}}return(i=a).\u0275fac=function(t){return new(t||i)(e.Y36(N.gz),e.Y36(N.F0),e.Y36(e.sBO),e.Y36(B),e.Y36(e.R0b),e.Y36(ue),e.Y36(q.s),e.Y36(e.Qsj),e.Y36(de.F),e.Y36(Ye),e.Y36(dn),e.Y36(Ae),e.Y36(un),e.Y36(Ie),e.Y36(U.z),e.Y36(w.e))},i.\u0275cmp=e.Xpm({type:i,selectors:[["worklenz-project-template-edit-view"]],viewQuery:function(t,n){if(1&t&&(e.Gf(Qi,5),e.Gf(Ri,5)),2&t){let o;e.iGM(o=e.CRH())&&(n.rows=o),e.iGM(o=e.CRH())&&(n.scrollPanels=o)}},hostBindings:function(t,n){1&t&&e.NdJ("scroll",function(r){return n.onScroll(r.target)})},decls:15,vars:10,consts:[[1,"container"],[1,"nz-header","mt-3","mb-4"],[1,"align-items-center"],[4,"nzSpaceItem"],["class","name-input position-relative",4,"nzSpaceItem"],[1,"top-section","mb-3","mt-3"],[3,"templateId","onGroupBy","onFilterSearch","onPhaseSettingsClick","onCreateStatusClick",4,"ngIf"],[3,"nzActive","nzLoading"],["class","pt-5 pb-5",4,"ngIf"],["cdkDropListGroup","",1,"tasks-wrapper"],["class","mb-3",4,"rxFor","rxForOf","rxForTrackBy"],[3,"templateId","show","showChange","onCreateOrUpdate","refresh",4,"ngIf"],["action","Create",3,"statusId","templateId","show","onCreateOrUpdate","showChange",4,"ngIf"],[3,"templateId","groups"],["nz-button","","nzType","text",1,"ant-page-header-back-button","d-flex","align-items-center",2,"font-size","16px",3,"click"],["nz-icon","","nzType","arrow-left","nzTheme","outline"],[1,"name-input","position-relative"],[3,"templateId","templateName"],[3,"templateId","onGroupBy","onFilterSearch","onPhaseSettingsClick","onCreateStatusClick"],[1,"pt-5","pb-5"],[1,"no-data-img-holder","mx-auto","mb-4"],["src","/assets/images/empty-box.webp","alt","",1,"img-fluid"],["nz-typography","",1,"mx-auto","d-block","text-center","no-data-text",2,"width","max-content",3,"ngSwitch"],[1,"mb-3"],[1,"container","px-0"],[3,"group","templateId","categories","toggle","onCreateOrUpdate"],[1,"panel","position-relative"],["panel",""],[1,"panel-left-border"],[1,"tasks-table","position-relative"],["cdkDropList","",1,"container","px-0","table-container","table-1",3,"id","cdkDropListConnectedTo","cdkDropListData","cdkDropListDropped","scroll"],["scrollPanel",""],[1,"border-right"],[3,"groupId","selectChange","phaseSettingsClick"],["class","tasks-empty-placeholder d-flex align-items-center mb-0 ps-3","nz-typography","","nzType","secondary",4,"ngIf"],[4,"ngFor","ngForOf","ngForTrackBy"],[1,"new-task-input"],["td1",""],[3,"templateId","groupId","focusChange",4,"ngIf"],["nz-typography","","nzType","secondary",1,"tasks-empty-placeholder","d-flex","align-items-center","mb-0","ps-3"],[3,"task","onShowSubTasks"],["row",""],[4,"ngIf"],[3,"task",4,"ngFor","ngForOf","ngForTrackBy"],[3,"task"],[1,"d-flex","inner-subtask-create","cursor-pointer","sub-task-background-color"],["td",""],[1,"new-subtask-divider"],[1,"w-100"],[3,"templateId","label","parentTask","groupId","subTaskInput","focusChange"],["subTaskInput",""],[3,"templateId","groupId","focusChange"],[3,"templateId","show","showChange","onCreateOrUpdate","refresh"],["action","Create",3,"statusId","templateId","show","onCreateOrUpdate","showChange"]],template:function(t,n){1&t&&(e.TgZ(0,"div",0)(1,"div",1)(2,"nz-space",2),e.YNc(3,Bi,3,0,"span",3),e.YNc(4,Hi,2,2,"span",4),e.qZA()(),e.TgZ(5,"nz-content")(6,"div",5),e.YNc(7,$i,1,1,"worklenz-group-filter",6),e.qZA(),e.TgZ(8,"nz-skeleton",7),e.YNc(9,ji,4,1,"div",8),e.TgZ(10,"div",9),e.YNc(11,ts,16,15,"div",10),e.qZA()()()(),e.YNc(12,ns,1,2,"worklenz-phase-settings-drawer",11),e.YNc(13,is,1,3,"worklenz-status-settings-drawer",12),e._UZ(14,"worklenz-context-menu",13)),2&t&&(e.xp6(7),e.Q6J("ngIf",n.templateId),e.xp6(1),e.Q6J("nzActive",!0)("nzLoading",n.loadingGroups),e.xp6(1),e.Q6J("ngIf",!n.groups.length),e.xp6(2),e.Q6J("rxForOf",n.groups)("rxForTrackBy",n.trackById),e.xp6(1),e.Q6J("ngIf",n.templateId),e.xp6(1),e.Q6J("ngIf",n.templateId),e.xp6(1),e.Q6J("templateId",n.templateId)("groups",n.groups))},dependencies:[h.sg,h.O5,h.RF,D.OK,M.Ls,O.ix,v.w,Q.ZU,H.NU,H.$1,$.ng,Ce.Wj,Qe,gn._,yn,Pn,hi,xi,Mi,Li,Ei,Yi],styles:['.task-status-color[_ngcontent-%COMP%]{border-width:1.4px;border-style:solid;border-radius:4px;width:1px}.task-name-color[_ngcontent-%COMP%]{height:20px;margin:auto;border-color:#a9a9a9;background-color:#a9a9a9;position:absolute;top:0;bottom:0;left:0;border-width:1.2px}.editable-cell[_ngcontent-%COMP%]{white-space:nowrap;max-width:255px;overflow:hidden;text-overflow:ellipsis}nz-date-picker[_ngcontent-%COMP%]{background:transparent}.expanded[_ngcontent-%COMP%]{transform:rotate(-90deg)}.dropdown-highlight[_ngcontent-%COMP%]{padding:1px}.highlight-col[_ngcontent-%COMP%]{border:1px solid #1890ff!important}.dropdown-highlight[_ngcontent-%COMP%]:hover{background-color:#d0eefa54;border:#5587f5 1px solid;border-radius:3px}.pointer-text[_ngcontent-%COMP%]{cursor:text}.plus-icon[_ngcontent-%COMP%]{display:none;position:absolute;right:0;z-index:9;background-color:#e2e7ea}tr[_ngcontent-%COMP%]:hover .plus-icon[_ngcontent-%COMP%]{display:block}.selected[_ngcontent-%COMP%], .selected[_ngcontent-%COMP%] .task-name[_ngcontent-%COMP%], .selected[_ngcontent-%COMP%] .task-drag-handler[_ngcontent-%COMP%]{background:rgba(24,144,255,.0784313725)!important}.sub-task-background-color[_ngcontent-%COMP%]{background-color:#f5f5f58a}.drop-down-btn[_ngcontent-%COMP%] .anticon-caret-down[_ngcontent-%COMP%]{color:#000000a6}div[_ngcontent-%COMP%]{box-sizing:border-box}.flex-table[_ngcontent-%COMP%]{display:flex;width:max-content}.table-container[_ngcontent-%COMP%]{overflow:auto;display:flex}.tasks-table[_ngcontent-%COMP%]{width:max-content;margin-left:3px;border-right:1px solid #f0f0f0}.column-trigger[_ngcontent-%COMP%]{background-color:#f4f5f7;height:40px;width:40px;justify-content:center;display:flex;align-items:center}.header[_ngcontent-%COMP%]{margin-bottom:0;position:sticky;top:0;background-color:#fff;z-index:2}.header[_ngcontent-%COMP%] .flex-row[_ngcontent-%COMP%]{padding:4px 11px;background-color:#fafafa;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0;border-right:1px solid #f0f0f0;display:flex;align-items:center;flex-direction:row}.br-right[_ngcontent-%COMP%]{border-right:1px solid #f0f0f0}.task-check[_ngcontent-%COMP%]{text-align:center;padding:8px 6px 8px 0!important;position:sticky;left:24px;z-index:1}.task-key[_ngcontent-%COMP%]{width:85px}.task-arrow[_ngcontent-%COMP%]{width:24px;padding:0!important;display:flex;align-items:center;border:none!important;position:sticky;left:47px;z-index:1}.task-name[_ngcontent-%COMP%]{width:450px;min-width:450px;position:sticky;left:71px;z-index:1;background-color:#fff}.task-name[_ngcontent-%COMP%] nz-filter-trigger[_ngcontent-%COMP%]{margin-left:auto}.task-description[_ngcontent-%COMP%]{width:225px}.task-progress[_ngcontent-%COMP%]{width:80px}.task-members[_ngcontent-%COMP%]{width:160px}.task-labels[_ngcontent-%COMP%]{width:210px}.task-status[_ngcontent-%COMP%], .task-priority[_ngcontent-%COMP%], .task-time-tracking[_ngcontent-%COMP%], .task-estimation[_ngcontent-%COMP%]{width:120px}.task-start-date[_ngcontent-%COMP%], .task-due-date[_ngcontent-%COMP%], .task-completed-date[_ngcontent-%COMP%], .task-created-date[_ngcontent-%COMP%], .task-update-date[_ngcontent-%COMP%]{width:150px}.task-drag-handler[_ngcontent-%COMP%]{padding:0 0 0 4px!important;width:24px;border-bottom:1px solid #f0f0f0;border-right:none!important;position:sticky;left:0}.inner-subtask-create[_ngcontent-%COMP%]{height:42px;display:flex;align-items:center;max-width:1316px;overflow:hidden;background-color:#fafafa;position:sticky;left:0;border-bottom:1px solid #eaeaea;border-top:1px solid #eaeaea}@media (max-width: 1400px){.inner-subtask-create[_ngcontent-%COMP%]{max-width:1136px}}@media (max-width: 1200px){.inner-subtask-create[_ngcontent-%COMP%]{max-width:956px}}.inner-subtask-create.highlight-col[_ngcontent-%COMP%]{background-color:#fff}.new-subtask-divider[_ngcontent-%COMP%]{width:50px}.new-subtask-divider.divider-large[_ngcontent-%COMP%]{width:135px}worklenz-quick-task[_ngcontent-%COMP%]{display:block}.overflow-x-auto[_ngcontent-%COMP%]{overflow-x:auto;overflow-y:hidden}.panel[_ngcontent-%COMP%]{padding:0;background-color:#fff;max-height:calc(100% + 8px);overflow:hidden;transition:max-height .1s ease-out;border-right:1px solid #f0f0f0}.panel-left-border[_ngcontent-%COMP%]{position:absolute;content:"";top:0;bottom:0;width:3px;z-index:3;border-bottom-left-radius:4px}.collapse.btn[_ngcontent-%COMP%] .collapse-icon[_ngcontent-%COMP%]{transition:transform .1s;transform:rotate(0)}.collapse.btn.active[_ngcontent-%COMP%] .collapse-icon[_ngcontent-%COMP%]{transition:transform .1s;transform:rotate(90deg)}.drop-down-btn[_ngcontent-%COMP%]{padding:4px 11px}.drop-down-btn[_ngcontent-%COMP%] .anticon-caret-down[_ngcontent-%COMP%]{color:#d9d9d9}.status-color[_ngcontent-%COMP%]{width:11px;height:11px;border-radius:2px;margin-right:7px}.drop-down-btn[_ngcontent-%COMP%] nz-badge[_ngcontent-%COMP%]{margin-top:-2px}.drop-down-btn.active[_ngcontent-%COMP%]{color:#1890ff;border-color:#1890ff;background-color:#e6f7ff}.drop-down-btn.active[_ngcontent-%COMP%] .drop-down-btn[_ngcontent-%COMP%] .anticon-caret-down[_ngcontent-%COMP%]{color:#1890ff}.ant-badge-count-sm[_ngcontent-%COMP%]{font-size:11px}.tab-name-edit[_ngcontent-%COMP%]{overflow:hidden}.tab-name-edit[_ngcontent-%COMP%] span[_ngcontent-%COMP%]{margin-right:0;font-size:14px}.tab-name-edit[_ngcontent-%COMP%] svg[_ngcontent-%COMP%]{margin-right:0}.new-task-input[_ngcontent-%COMP%]{padding-left:4px;padding-top:4px;padding-bottom:4px;border-bottom:1px solid #f0f0f0;border-top:1px solid #f0f0f0;max-width:1316px;overflow-x:auto}@media (max-width: 1400px){.new-task-input[_ngcontent-%COMP%]{max-width:1136px}}@media (max-width: 1200px){.new-task-input[_ngcontent-%COMP%]{max-width:956px}}.tasks-empty-placeholder[_ngcontent-%COMP%]{width:100%;height:42px;background:#fafafa}.no-data-img-holder[_ngcontent-%COMP%]{width:100px;margin-top:42px}.add-field-button[_ngcontent-%COMP%]{position:absolute;top:46px;right:0}.name-input[_ngcontent-%COMP%]{max-width:1270px;width:1270px;display:block}@media (max-width: 1400px){.name-input[_ngcontent-%COMP%]{max-width:1090px;width:1090px}}@media (max-width: 1200px){.name-input[_ngcontent-%COMP%]{max-width:910px;width:910px}}'],changeDetection:0}),a})()}];let as=(()=>{var i;class a{}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275mod=e.oAB({type:i}),i.\u0275inj=e.cJS({imports:[N.Bz.forChild(os),N.Bz]}),a})();var rs=c(3626),ls=c(79382),cs=c(82669),ps=c(62831),ds=c(48128),us=c(64345),_s=c(49388);let uo=(()=>{var i;class a{}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275mod=e.oAB({type:i}),i.\u0275inj=e.cJS({imports:[_s.vT,h.ez,p.u5,ps.ud,J.cg,ds.W,us.YI,M.PV,O.sL]}),a})();var _o=c(48522),mo=c(29666),ho=c(52682);let go=(()=>{var i;class a{}return(i=a).\u0275fac=function(t){return new(t||i)},i.\u0275mod=e.oAB({type:i}),i.\u0275inj=e.cJS({providers:[Oe.g],imports:[h.ez,as,D.wm,V.vh,Y.KJ,rs.lt,M.PV,ls.we,p.UX,x.U5,P.o7,O.sL,cs.j,z.HQ,fe.Qp,Q.ZJ,T.ip,H.zf,J.cg,$.H0,K.LV,p.u5,uo,Me.l,ie.mS,E.b1,ze.X,Je.x,Te._p,Ge.S,De.Rt,_o.a,mo.V,we.Wr,Qe,ho.Hb,He.YS,$e.o,Be.PG,je.Zf,ke.BL,Fe.S]}),a})()},80697:(ye,se,c)=>{c.d(se,{I:()=>U});var h=c(26857),N=c(10708),e=c(65879),w=c(69862);let U=(()=>{var D;class Y extends h.P{constructor(T){super(),this.http=T,this.root=`${this.API_BASE_URL}/project-templates`}createCustomTemplate(T){return this._post(this.http,`${this.root}/custom-template`,T)}createFromTemplate(T){return this._post(this.http,`${this.root}/import-template`,T)}createFromCustomTemplate(T){return this._post(this.http,`${this.root}/import-custom-template`,T)}createTemplates(){return this._get(this.http,`${this.root}/create`)}getWorklenzTemplates(){return this._get(this.http,`${this.root}/worklenz-templates`)}getWorklenzTemplateById(T){return this._get(this.http,`${this.root}/worklenz-templates/${T}`)}getWorklenzCustomTemplates(){return this._get(this.http,`${this.root}/custom-templates`)}delete(T){return(0,N.n)(this.http.delete(`${this.root}/${T}`))}setupAccount(T){return this._post(this.http,`${this.root}/setup`,T)}}return(D=Y).\u0275fac=function(T){return new(T||D)(e.LFG(w.eN))},D.\u0275prov=e.Yz7({token:D,factory:D.\u0275fac,providedIn:"root"}),Y})()},26254:(ye,se,c)=>{c.d(se,{$:()=>L,$O:()=>ge,Jp:()=>O,KJ:()=>Pe,u9:()=>M,yG:()=>Z});var h=c(49388),N=c(96814),e=c(65879),w=c(8324),U=c(62595),D=c(97582),Y=c(78645),v=c(59773),T=c(37398),re=c(40874),X=c(1608),ee=c(28802);function ve(u,g){if(1&u&&(e.ynx(0),e._UZ(1,"span",9),e.BQk()),2&u){const C=g.$implicit,f=e.oxw(2);e.xp6(1),e.Q6J("nzType",C||f.getBackIcon())}}function m(u,g){if(1&u){const C=e.EpF();e.TgZ(0,"div",6),e.NdJ("click",function(){e.CHM(C);const F=e.oxw();return e.KtG(F.onBack())}),e.TgZ(1,"div",7),e.YNc(2,ve,2,1,"ng-container",8),e.qZA()()}if(2&u){const C=e.oxw();e.xp6(2),e.Q6J("nzStringTemplateOutlet",C.nzBackIcon)}}function p(u,g){if(1&u&&(e.ynx(0),e._uU(1),e.BQk()),2&u){const C=e.oxw(2);e.xp6(1),e.Oqu(C.nzTitle)}}function me(u,g){if(1&u&&(e.TgZ(0,"span",10),e.YNc(1,p,2,1,"ng-container",8),e.qZA()),2&u){const C=e.oxw();e.xp6(1),e.Q6J("nzStringTemplateOutlet",C.nzTitle)}}function b(u,g){1&u&&e.Hsn(0,6,["*ngIf","!nzTitle"])}function A(u,g){if(1&u&&(e.ynx(0),e._uU(1),e.BQk()),2&u){const C=e.oxw(2);e.xp6(1),e.Oqu(C.nzSubtitle)}}function I(u,g){if(1&u&&(e.TgZ(0,"span",11),e.YNc(1,A,2,1,"ng-container",8),e.qZA()),2&u){const C=e.oxw();e.xp6(1),e.Q6J("nzStringTemplateOutlet",C.nzSubtitle)}}function he(u,g){1&u&&e.Hsn(0,7,["*ngIf","!nzSubtitle"])}const Se=[[["nz-breadcrumb","nz-page-header-breadcrumb",""]],[["nz-avatar","nz-page-header-avatar",""]],[["nz-page-header-tags"],["","nz-page-header-tags",""]],[["nz-page-header-extra"],["","nz-page-header-extra",""]],[["nz-page-header-content"],["","nz-page-header-content",""]],[["nz-page-header-footer"],["","nz-page-header-footer",""]],[["nz-page-header-title"],["","nz-page-header-title",""]],[["nz-page-header-subtitle"],["","nz-page-header-subtitle",""]]],V=["nz-breadcrumb[nz-page-header-breadcrumb]","nz-avatar[nz-page-header-avatar]","nz-page-header-tags, [nz-page-header-tags]","nz-page-header-extra, [nz-page-header-extra]","nz-page-header-content, [nz-page-header-content]","nz-page-header-footer, [nz-page-header-footer]","nz-page-header-title, [nz-page-header-title]","nz-page-header-subtitle, [nz-page-header-subtitle]"];let M=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275dir=e.lG2({type:u,selectors:[["nz-page-header-title"],["","nz-page-header-title",""]],hostAttrs:[1,"ant-page-header-heading-title"],exportAs:["nzPageHeaderTitle"]}),g})(),Z=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275dir=e.lG2({type:u,selectors:[["nz-page-header-subtitle"],["","nz-page-header-subtitle",""]],hostAttrs:[1,"ant-page-header-heading-sub-title"],exportAs:["nzPageHeaderSubtitle"]}),g})(),O=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275dir=e.lG2({type:u,selectors:[["nz-page-header-extra"],["","nz-page-header-extra",""]],hostAttrs:[1,"ant-page-header-heading-extra"],exportAs:["nzPageHeaderExtra"]}),g})(),L=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275dir=e.lG2({type:u,selectors:[["nz-page-header-footer"],["","nz-page-header-footer",""]],hostAttrs:[1,"ant-page-header-footer"],exportAs:["nzPageHeaderFooter"]}),g})(),Q=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275dir=e.lG2({type:u,selectors:[["nz-breadcrumb","nz-page-header-breadcrumb",""]],exportAs:["nzPageHeaderBreadcrumb"]}),g})(),ge=(()=>{var u;class g{constructor(f,F,pe,te,z,fe){this.location=f,this.nzConfigService=F,this.elementRef=pe,this.nzResizeObserver=te,this.cdr=z,this.directionality=fe,this._nzModuleName="pageHeader",this.nzBackIcon=null,this.nzGhost=!0,this.nzBack=new e.vpe,this.compact=!1,this.destroy$=new Y.x,this.dir="ltr"}ngOnInit(){this.directionality.change?.pipe((0,v.R)(this.destroy$)).subscribe(f=>{this.dir=f,this.cdr.detectChanges()}),this.dir=this.directionality.value}ngAfterViewInit(){this.nzResizeObserver.observe(this.elementRef).pipe((0,T.U)(([f])=>f.contentRect.width),(0,v.R)(this.destroy$)).subscribe(f=>{this.compact=f<768,this.cdr.markForCheck()})}onBack(){if(this.nzBack.observers.length)this.nzBack.emit();else{if(!this.location)throw new Error(`${X.Bq} you should import 'RouterModule' or register 'Location' if you want to use 'nzBack' default event!`);this.location.back()}}ngOnDestroy(){this.destroy$.next(),this.destroy$.complete()}getBackIcon(){return"rtl"===this.dir?"arrow-right":"arrow-left"}}return(u=g).\u0275fac=function(f){return new(f||u)(e.Y36(N.Ye,8),e.Y36(re.jY),e.Y36(e.SBq),e.Y36(ee.D3),e.Y36(e.sBO),e.Y36(h.Is,8))},u.\u0275cmp=e.Xpm({type:u,selectors:[["nz-page-header"]],contentQueries:function(f,F,pe){if(1&f&&(e.Suo(pe,L,5),e.Suo(pe,Q,5)),2&f){let te;e.iGM(te=e.CRH())&&(F.nzPageHeaderFooter=te.first),e.iGM(te=e.CRH())&&(F.nzPageHeaderBreadcrumb=te.first)}},hostAttrs:[1,"ant-page-header"],hostVars:10,hostBindings:function(f,F){2&f&&e.ekj("has-footer",F.nzPageHeaderFooter)("ant-page-header-ghost",F.nzGhost)("has-breadcrumb",F.nzPageHeaderBreadcrumb)("ant-page-header-compact",F.compact)("ant-page-header-rtl","rtl"===F.dir)},inputs:{nzBackIcon:"nzBackIcon",nzTitle:"nzTitle",nzSubtitle:"nzSubtitle",nzGhost:"nzGhost"},outputs:{nzBack:"nzBack"},exportAs:["nzPageHeader"],ngContentSelectors:V,decls:13,vars:5,consts:[[1,"ant-page-header-heading"],[1,"ant-page-header-heading-left"],["class","ant-page-header-back",3,"click",4,"ngIf"],["class","ant-page-header-heading-title",4,"ngIf"],[4,"ngIf"],["class","ant-page-header-heading-sub-title",4,"ngIf"],[1,"ant-page-header-back",3,"click"],["role","button","tabindex","0",1,"ant-page-header-back-button"],[4,"nzStringTemplateOutlet"],["nz-icon","","nzTheme","outline",3,"nzType"],[1,"ant-page-header-heading-title"],[1,"ant-page-header-heading-sub-title"]],template:function(f,F){1&f&&(e.F$t(Se),e.Hsn(0),e.TgZ(1,"div",0)(2,"div",1),e.YNc(3,m,3,1,"div",2),e.Hsn(4,1),e.YNc(5,me,2,1,"span",3),e.YNc(6,b,1,0,"ng-content",4),e.YNc(7,I,2,1,"span",5),e.YNc(8,he,1,0,"ng-content",4),e.Hsn(9,2),e.qZA(),e.Hsn(10,3),e.qZA(),e.Hsn(11,4),e.Hsn(12,5)),2&f&&(e.xp6(3),e.Q6J("ngIf",null!==F.nzBackIcon),e.xp6(2),e.Q6J("ngIf",F.nzTitle),e.xp6(1),e.Q6J("ngIf",!F.nzTitle),e.xp6(1),e.Q6J("ngIf",F.nzSubtitle),e.xp6(1),e.Q6J("ngIf",!F.nzSubtitle))},dependencies:[N.O5,w.f,U.Ls],encapsulation:2,changeDetection:0}),(0,D.gn)([(0,re.oS)()],g.prototype,"nzGhost",void 0),g})(),Pe=(()=>{var u;class g{}return(u=g).\u0275fac=function(f){return new(f||u)},u.\u0275mod=e.oAB({type:u}),u.\u0275inj=e.cJS({imports:[h.vT,N.ez,w.T,U.PV]}),g})()}}]); \ No newline at end of file diff --git a/worklenz-backend/src/public/152.89824b7edf9e2b3f.js b/worklenz-backend/src/public/152.89824b7edf9e2b3f.js new file mode 100644 index 00000000..ce8b9a4d --- /dev/null +++ b/worklenz-backend/src/public/152.89824b7edf9e2b3f.js @@ -0,0 +1,129 @@ +(self.webpackChunkworklenz=self.webpackChunkworklenz||[]).push([[152],{79152:(bm,ao,oe)=>{"use strict";oe.r(ao),oe.d(ao,{ProjectsModule:()=>ZC});var Ie=oe(96814),wt=oe(35420),ce=oe(15861),e=oe(65879),Ot=oe(21406),yr=oe(13490),st=oe(69649),pt=oe(86408),Ve=oe(27782),Pe=oe(60095),Pt=oe(71993),ra=oe(55012),vd=oe(97192),hn=oe(10095),mn=oe(3599),nA=oe(20824),Vt=oe(42840),Wt=oe(70855),an=oe(41958),AA=oe(55695),QA=oe(82669),lA=oe(43389),zn=oe(73460),cn=oe(62787);const Mc=["nameInput"];function $h(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"li",14),e.NdJ("click",function(){const s=e.CHM(A).$implicit,o=e.oxw(2);return e.KtG(o.setColorCode(s))}),e.TgZ(1,"nz-tag",15),e._uU(2,"\xa0 "),e.qZA()()}if(2&r){const A=i.$implicit;e.xp6(1),e.Q6J("nzColor",A)}}function ep(r,i){if(1&r){const A=e.EpF();e.ynx(0),e.TgZ(1,"nz-spin",2)(2,"form",3),e.NdJ("submit",function(){e.CHM(A);const n=e.oxw();return e.KtG(n.submit())}),e.TgZ(3,"nz-form-item")(4,"nz-form-label",4),e._uU(5,"Name"),e.qZA(),e.TgZ(6,"nz-form-control",5),e._UZ(7,"input",6,7),e.qZA()(),e.TgZ(9,"nz-form-item")(10,"nz-form-label",8),e._uU(11,"Folder Color "),e.TgZ(12,"nz-tag",9),e._uU(13,"\xa0 "),e.qZA()(),e.TgZ(14,"nz-dropdown-menu",null,10)(16,"ul",11),e.YNc(17,$h,3,1,"li",12),e.qZA()()(),e.TgZ(18,"button",13),e._uU(19),e.qZA()()(),e.BQk()}if(2&r){const A=e.MAs(15),t=e.oxw();e.xp6(1),e.Q6J("nzSpinning",t.loading),e.xp6(1),e.Q6J("formGroup",t.form)("nzLayout","vertical"),e.xp6(2),e.Q6J("nzSpan",null),e.xp6(2),e.Q6J("nzSpan",null)("nzErrorTip","Please enter a name!"),e.xp6(1),e.Q6J("formControlName","name"),e.xp6(3),e.Q6J("nzSpan",null),e.xp6(2),e.Q6J("nzColor",t.activeColor)("nzDropdownMenu",A)("nzTrigger","click"),e.xp6(5),e.Q6J("ngForOf",t.COLOR_CODES),e.xp6(1),e.Q6J("nzType","primary")("disabled",!t.valid),e.xp6(1),e.Oqu(t.buttonText)}}let Cd=(()=>{var r;class i{get activeColor(){return this.form.controls.color_code.value}get title(){return this.folderId?"Update the folder":"Create a folder"}get buttonText(){return this.folderId?"Update":"Create"}get valid(){return this.form.valid}constructor(t,n,s,o,h){this.ngZone=t,this.cdr=n,this.fb=s,this.api=o,this.service=h,this.folderId=null,this.show=!1,this.loading=!1,this.COLOR_CODES=Ot.lD,this.createCallback=null,this.form=this.fb.group({name:[null,[Pe.kI.required]],color_code:[null]}),this.service.onCreateInvoke.pipe((0,Pt.sL)()).subscribe(p=>{this.createCallback=p,this.open()})}open(){this.show=!0,this.cdr.markForCheck()}close(){this.show=!1,this.form.reset({name:null,color_code:null}),this.invokeCreate(),this.cdr.markForCheck()}invokeCreate(t){this.createCallback&&(this.createCallback(t),this.createCallback=null)}submit(){var t=this;return(0,ce.Z)(function*(){if(!t.form.invalid){try{t.loading=!0;const n={name:t.form.value.name,color_code:t.form.value.color_code},s=yield t.api.create(n);s.done&&(t.invokeCreate(s.body),t.close()),t.loading=!1}catch{t.loading=!1}t.cdr.markForCheck()}})()}setColorCode(t){this.form.controls.color_code.setValue(t)}onVisibilityChange(t){t&&this.setFocusToNameInput()}setFocusToNameInput(){this.ngZone.runOutsideAngular(()=>{setTimeout(()=>{const t=this.nameInput.nativeElement;t&&t.focus()},100)})}}return(r=i).\u0275fac=function(t){return new(t||r)(e.Y36(e.R0b),e.Y36(e.sBO),e.Y36(Pe.qu),e.Y36(ra.Y),e.Y36(vd.I))},r.\u0275cmp=e.Xpm({type:r,selectors:[["worklenz-projects-folder-form-drawer"]],viewQuery:function(t,n){if(1&t&&e.Gf(Mc,5),2&t){let s;e.iGM(s=e.CRH())&&(n.nameInput=s.first)}},inputs:{folderId:"folderId"},decls:2,vars:4,consts:[[3,"nzClosable","nzTitle","nzVisible","nzPlacement","nzOnClose","nzVisibleChange"],[4,"nzDrawerContent"],[3,"nzSpinning"],["nz-form","",3,"formGroup","nzLayout","submit"],["nzRequired","",3,"nzSpan"],[3,"nzSpan","nzErrorTip"],["nz-input","","placeholder","Enter your folder name",3,"formControlName"],["nameInput",""],["nzRequired","",1,"pb-0",3,"nzSpan"],["nz-dropdown","",1,"ms-2","rounded-circle","cursor-pointer",2,"width","20px","height","20px",3,"nzColor","nzDropdownMenu","nzTrigger"],["menu","nzDropdownMenu"],["nz-menu","","nzSelectable","",2,"max-height","200px","overflow","hidden","overflow-y","auto"],["nz-menu-item","",3,"click",4,"ngFor","ngForOf"],["nz-button","","nzBlock","","type","submit",3,"nzType","disabled"],["nz-menu-item","",3,"click"],[1,"me-1","w-100","rounded-pill",2,"height","16px !important","width","16px !important",3,"nzColor"]],template:function(t,n){1&t&&(e.TgZ(0,"nz-drawer",0),e.NdJ("nzOnClose",function(){return n.close()})("nzVisibleChange",function(o){return n.onVisibilityChange(o)}),e.YNc(1,ep,20,15,"ng-container",1),e.qZA()),2&t&&e.Q6J("nzClosable",!0)("nzTitle",n.title)("nzVisible",n.show)("nzPlacement","right")},dependencies:[Ie.sg,Pe._Y,Pe.Fj,Pe.JJ,Pe.JL,Pe.sg,Pe.u,hn.t3,hn.SK,mn.Lr,mn.Nx,mn.iK,mn.Fd,nA.Zp,Vt.ix,Wt.w,an.dQ,AA.j,QA.W,lA.Vz,lA.SQ,zn.wO,zn.r9,cn.cm,cn.RR],changeDetection:0}),i})();var Ec=oe(16406),ia=oe(90586),ji=oe(31215),yn=oe(32333),JA=oe(64532),Bd=oe(53980),Lc=oe(80697),YA=oe(55416),tp=oe(52732),Tr=oe(26254),xt=oe(13740),zt=oe(62595),Ut=oe(8083),qA=oe(62612),ms=oe(48128),Zt=oe(96109),Mn=oe(24139),DA=oe(82962),HA=oe(92574),fr=oe(33640),Ri=oe(22114),Nc=oe(98421),FA=oe(99183),VA=oe(76271);let bd=(()=>{var r;class i{transform(t){return`Click to filter by "${t}".`}}return(r=i).\u0275fac=function(t){return new(t||r)},r.\u0275pipe=e.Yjl({name:"projectFilterByTooltip",type:r,pure:!0}),i})();const Oc=["displayModeTemplate"];function np(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"button",25),e.NdJ("click",function(){e.CHM(A);const n=e.oxw();return e.KtG(n.refresh())}),e._UZ(1,"span",26),e.qZA()}if(2&r){const A=e.oxw();e.Q6J("nzShape","circle")("nzTooltipTitle","Refresh projects")("nzType","default"),e.xp6(1),e.Q6J("nzSpin",A.loading)("nzTheme","outline")("nzType","sync")}}function co(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"nz-segmented",27),e.NdJ("ngModelChange",function(n){e.CHM(A);const s=e.oxw();return e.KtG(s.filterIndex=n)})("nzValueChange",function(n){e.CHM(A);const s=e.oxw();return e.KtG(s.onFilterChange(n))}),e.qZA()}if(2&r){const A=e.oxw();e.Q6J("ngModel",A.filterIndex)("nzOptions",A.filters)}}function Pc(r,i){1&r&&e._UZ(0,"span",32)}function sa(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"form",28),e.NdJ("ngSubmit",function(){e.CHM(A);const n=e.oxw();return e.KtG(n.searchProjects())}),e.TgZ(1,"nz-input-group",29),e._UZ(2,"input",30),e.qZA(),e.YNc(3,Pc,1,0,"ng-template",null,31,e.W1O),e.qZA()}if(2&r){const A=e.MAs(4),t=e.oxw();e.Q6J("formGroup",t.searchForm)("nzLayout","vertical"),e.xp6(1),e.Q6J("nzSuffix",A),e.xp6(1),e.Q6J("formControlName","search")}}function lo(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"button",40),e.NdJ("click",function(){e.CHM(A);const n=e.oxw(3);return e.KtG(n.openProjectForm())}),e._UZ(1,"span",41),e._uU(2,"Create Project"),e.qZA()}2&r&&e.Q6J("nzType","primary")}function Ap(r,i){if(1&r&&(e.TgZ(0,"button",42),e._UZ(1,"span",43),e.qZA()),2&r){e.oxw(2);const A=e.MAs(3);e.Q6J("nzType","primary")("nzDropdownMenu",A)("nzTrigger","click")("nzPlacement","bottomRight")}}function xd(r,i){if(1&r&&(e.TgZ(0,"span")(1,"nz-button-group"),e.YNc(2,lo,3,1,"button",38),e.YNc(3,Ap,2,4,"button",39),e.qZA()()),2&r){const A=e.oxw(2);e.xp6(2),e.Q6J("ngIf",A.isOwnerOrAdmin()),e.xp6(1),e.Q6J("ngIf",A.isOwnerOrAdmin())}}function Dc(r,i){if(1&r){const A=e.EpF();e.ynx(0),e.YNc(1,xd,4,2,"span",33),e.TgZ(2,"nz-dropdown-menu",null,34)(4,"ul",35)(5,"li",36),e.NdJ("click",function(){e.CHM(A);const n=e.oxw();return e.KtG(n.openTemplateSelector())}),e._UZ(6,"span",37),e._uU(7," Create from template "),e.qZA()()(),e.BQk()}}function uo(r,i){if(1&r&&(e.TgZ(0,"div"),e._uU(1),e.ALo(2,"date"),e.qZA()),2&r){const A=e.oxw(3).$implicit;e.xp6(1),e.hij("Start date : ",e.xi3(2,1,A.start_date,"mediumDate"),"")}}function Hc(r,i){if(1&r&&(e.TgZ(0,"div"),e._uU(1),e.ALo(2,"date"),e.qZA()),2&r){const A=e.oxw(3).$implicit;e.xp6(1),e.hij("End date : ",e.xi3(2,1,A.end_date,"mediumDate"),"")}}function _s(r,i){if(1&r&&(e.YNc(0,uo,3,4,"div",5),e.YNc(1,Hc,3,4,"div",5)),2&r){const A=e.oxw(2).$implicit;e.Q6J("ngIf",A.start_date),e.xp6(1),e.Q6J("ngIf",A.end_date)}}function ZA(r,i){if(1&r&&(e.ynx(0),e.TgZ(1,"span",60),e._UZ(2,"span",12),e.qZA(),e.YNc(3,_s,2,2,"ng-template",null,61,e.W1O),e.BQk()),2&r){const A=e.MAs(4);e.xp6(1),e.Q6J("nzType","secondary")("nzTooltipTitle",A),e.xp6(1),e.Q6J("nzType","calendar")("nzTheme","outline")}}function ho(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"nz-tag",62),e.NdJ("click",function(n){e.CHM(A);const s=e.oxw().$implicit,o=e.oxw();return n.stopPropagation(),e.KtG(o.filterByCategory(s.category_id))}),e.ALo(1,"projectFilterByTooltip"),e._uU(2),e.qZA()}if(2&r){const A=e.oxw().$implicit;e.Q6J("nzTooltipTitle",e.lcZ(1,3,A.category_name))("nzColor",A.category_color),e.xp6(2),e.Oqu(A.category_name)}}function po(r,i){1&r&&(e.ynx(0),e._uU(1,"-"),e.BQk())}function ws(r,i){1&r&&(e.ynx(0),e._uU(1," N/A "),e.BQk())}const yd=function(){return[]};function Gi(r,i){if(1&r&&e._UZ(0,"worklenz-avatars",63),2&r){const A=e.oxw().$implicit;e.Q6J("names",A.names||e.DdM(1,yd))}}function rp(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"button",67),e.NdJ("click",function(n){e.CHM(A);const s=e.oxw(2).$implicit,o=e.oxw();return n.stopPropagation(),e.KtG(o.openProjectForm(s.id))}),e._UZ(1,"span",53),e.qZA()}2&r&&(e.Q6J("nzSize","small")("nzTooltipPlacement","top")("nzTooltipTitle","Settings")("nzType","default"),e.xp6(1),e.Q6J("nzType","setting"))}function oa(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"button",68),e.NdJ("click",function(n){e.CHM(A);const s=e.oxw(2).$implicit,o=e.oxw();return n.stopPropagation(),e.KtG(o.toggleArchive(s))}),e._UZ(1,"span",12),e.qZA()}if(2&r){const A=e.oxw(2).$implicit;e.Q6J("nzSize","small")("nzTooltipPlacement","top")("nzTooltipTitle",A.archived?"Remove from archive":"Archive")("nzLoading",A.loading)("nzType","default"),e.xp6(1),e.Q6J("nzType","inbox")("nzTheme","outline")}}function aa(r,i){1&r&&(e.TgZ(0,"div",64)(1,"nz-space"),e.YNc(2,rp,2,5,"button",65),e.YNc(3,oa,2,7,"button",66),e.qZA()())}function Zc(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"tr",44),e.NdJ("click",function(){const s=e.CHM(A).$implicit,o=e.oxw();return e.KtG(o.selectProject(s.id,s.team_member_default_view?s.team_member_default_view:"TASK_LIST"))}),e.TgZ(1,"td",45)(2,"div",46)(3,"nz-rate",47),e.NdJ("click",function(n){const o=e.CHM(A).$implicit,h=e.oxw();return n.stopPropagation(),e.KtG(h.toggleFavorite(o.id))}),e.qZA(),e.TgZ(4,"span",48),e._UZ(5,"nz-badge",49),e._uU(6),e.YNc(7,ZA,5,4,"ng-container",5),e.qZA()()(),e.TgZ(8,"td"),e._uU(9),e.qZA(),e.TgZ(10,"td"),e.ynx(11,50),e.YNc(12,ho,3,5,"nz-tag",51),e.YNc(13,po,2,0,"ng-container",52),e.BQk(),e.qZA(),e.TgZ(14,"td"),e._UZ(15,"span",53),e.ALo(16,"safeString"),e._uU(17),e.qZA(),e.TgZ(18,"td"),e._UZ(19,"nz-progress",54),e.qZA(),e.TgZ(20,"td")(21,"span",55),e.ALo(22,"date"),e._uU(23),e.qZA()(),e.TgZ(24,"td"),e.YNc(25,ws,2,0,"ng-container",56),e.YNc(26,Gi,1,2,"ng-template",null,57,e.W1O),e.qZA(),e.TgZ(28,"td",58),e.YNc(29,aa,4,0,"div",59),e.qZA()()}if(2&r){const A=i.$implicit,t=e.MAs(27),n=e.oxw();e.xp6(3),e.Q6J("nzTooltipTitle",A.favorite?"Remove from favorites":"Add to favorites")("nzCount",1)("ngModel",A.favorite?1:0),e.xp6(2),e.Q6J("nzColor",A.color_code)("nzSize","default"),e.xp6(1),e.hij(" ",A.name," "),e.xp6(1),e.Q6J("ngIf",A.start_date||A.end_date),e.xp6(2),e.Oqu(A.client_name||"-"),e.xp6(2),e.Q6J("ngSwitch",!!A.category_name),e.xp6(1),e.Q6J("ngSwitchCase",!0),e.xp6(1),e.Q6J("ngSwitchCase",!1),e.xp6(2),e.Udp("color",A.status_color),e.Q6J("nzType",e.lcZ(16,25,A.status_icon)),e.xp6(2),e.hij(" ",A.status," "),e.xp6(2),e.Q6J("nzPercent",A.progress)("nzStrokeColor",A.color_code)("nzTooltipTitle",n.getTaskProgressTitle(A)),e.xp6(2),e.Q6J("nzTooltipTitle",e.xi3(22,27,A.updated_at,"medium"))("nzTooltipMouseEnterDelay",1),e.xp6(2),e.Oqu(A.updated_at_string),e.xp6(2),e.Q6J("ngIf",!(null!=A.names&&A.names.length))("ngIfElse",t),e.xp6(3),e.Q6J("nzAlign","center"),e.xp6(1),e.Q6J("ngIf",n.isOwnerOrAdmin())}}function jc(r,i){1&r&&(e.ynx(0),e._uU(1,"The team currently has no active projects."),e.BQk())}function Rc(r,i){1&r&&(e.ynx(0),e._uU(1,"You're yet to mark any projects as favorites."),e.BQk())}function fo(r,i){1&r&&(e.ynx(0),e._uU(1,"The selected team has no archived projects."),e.BQk())}function Gc(r,i){if(1&r&&(e.TgZ(0,"div",69)(1,"div",70),e._UZ(2,"img",71),e.qZA(),e.TgZ(3,"div",72),e.YNc(4,jc,2,0,"ng-container",52),e.YNc(5,Rc,2,0,"ng-container",52),e.YNc(6,fo,2,0,"ng-container",52),e.qZA()()),2&r){const A=e.oxw();e.xp6(3),e.Q6J("ngSwitch",A.filterIndex),e.xp6(1),e.Q6J("ngSwitchCase",0),e.xp6(1),e.Q6J("ngSwitchCase",1),e.xp6(1),e.Q6J("ngSwitchCase",2)}}function ca(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"li",74),e.NdJ("nzCheckedChange",function(n){const o=e.CHM(A).$implicit;return e.KtG(o.selected=n)})("nzCheckedChange",function(){e.CHM(A);const n=e.oxw(2);return e.KtG(n.onCategoryFilterChange())}),e._uU(1),e.qZA()}if(2&r){const A=i.$implicit;e.Q6J("nzChecked",A.selected),e.xp6(1),e.Oqu(A.name)}}function la(r,i){if(1&r&&(e.ynx(0),e.YNc(1,ca,2,2,"li",73),e.BQk()),2&r){const A=e.oxw();e.xp6(1),e.Q6J("ngForOf",A.categories)}}function Kc(r,i){1&r&&(e.TgZ(0,"li",75),e._uU(1,"No categories found"),e.qZA())}function Td(r,i){if(1&r){const A=e.EpF();e.TgZ(0,"li",74),e.NdJ("nzCheckedChange",function(n){const o=e.CHM(A).$implicit;return e.KtG(o.selected=n)})("nzCheckedChange",function(){e.CHM(A);const n=e.oxw(2);return e.KtG(n.onStatusFilterChange())}),e._uU(1),e.qZA()}if(2&r){const A=i.$implicit;e.Q6J("nzChecked",A.selected),e.xp6(1),e.Oqu(A.name)}}function ip(r,i){if(1&r&&(e.ynx(0),e.YNc(1,Td,2,2,"li",73),e.BQk()),2&r){const A=e.oxw();e.xp6(1),e.Q6J("ngForOf",A.statuses)}}function sp(r,i){1&r&&(e.TgZ(0,"li",75),e._uU(1,"No categories found"),e.qZA())}const kd=function(){return{rows:7}};let jt=(()=>{var r;class i{get filterIndex(){return+(localStorage.getItem(this.FILTER_INDEX_KEY)||0)}set filterIndex(t){localStorage.setItem(this.FILTER_INDEX_KEY,t.toString())}get displayAs(){return+(localStorage.getItem(this.DISPLAY_MODE_KEY)||0)}set displayAs(t){localStorage.setItem(this.DISPLAY_MODE_KEY,t.toString())}get profile(){return this.auth.getCurrentSession()}constructor(t,n,s,o,h,p,_,B,x,y,v,M){this.app=t,this.api=n,this.fb=s,this.auth=o,this.router=h,this.route=p,this.utilsService=_,this.categoriesApi=B,this.templatesApi=x,this.cdr=y,this.list=v,this.statusesApi=M,this.FILTER_INDEX_KEY="worklenz.projects.filter_index",this.DISPLAY_MODE_KEY="worklenz.projects.display_as",this.projectsModel={},this.categories=[],this.statuses=[],this.expandSet=new Set,this.total=0,this.pageSize=Ot.L8,this.pageIndex=1,this.paginationSizes=[5,10,15,20,50,100],this.sortField=null,this.sortOrder=null,this.categoriesFilterString=null,this.statusesFilterString=null,this.loading=!0,this.loadingCategories=!1,this.loadingStatuses=!1,this.showCategoriesFilter=!1,this.showStatusesFilter=!1,this.filteredByCategory=!1,this.filteredByStatus=!1,this.filters=["All","Favorites","Archived"],this.displayModes=[{label:"List",value:"list",useTemplate:!0},{label:"Folder",value:"folder",useTemplate:!0}],this.app.setTitle("Projects"),this.trackVisit(),this.pageSize=+(this.route.snapshot.queryParamMap.get("size")||Ot.L8),this.pageIndex=+(this.route.snapshot.queryParamMap.get("index")||1)}ngOnInit(){var t=this;return(0,ce.Z)(function*(){t.searchForm=t.fb.group({search:[]}),t.searchForm.valueChanges.subscribe(()=>{t.searchProjects()}),t.getCategories(),t.getStatuses()})()}getFilterIndexText(){return this.filters[this.filterIndex]}refresh(){pt.s.track(Ve.vQ),this.getCategories(),this.getProjects()}import(){var t=this;return(0,ce.Z)(function*(){yield t.templatesApi.createTemplates()})()}getCategories(){var t=this;return(0,ce.Z)(function*(){try{t.loadingCategories=!0;const n=yield t.categoriesApi.get();n.done&&(t.categories=n.body),t.loadingCategories=!1}catch{t.loadingCategories=!1}t.cdr.markForCheck()})()}getStatuses(){var t=this;return(0,ce.Z)(function*(){try{t.loadingStatuses=!0;const n=yield t.statusesApi.get();n.done&&(t.statuses=n.body),t.loadingStatuses=!1}catch{t.loadingStatuses=!1}t.cdr.markForCheck()})()}getProjects(t=!1){var n=this;return(0,ce.Z)(function*(){try{t||(n.loading=!0);const s=yield n.api.getByConfig({index:n.pageIndex,size:n.pageSize,field:n.sortField,order:n.sortOrder,search:n.searchForm.value.search,filter:n.filterIndex.toString(),categories:n.categoriesFilterString,statuses:n.statusesFilterString});s.done&&n.handleProjectsResponse(s),n.loading=!1}catch(s){n.loading=!1,(0,st.tu)(s)}n.cdr.markForCheck()})()}handleProjectsResponse(t){this.projectsModel=t.body,this.total=this.projectsModel.total||0,this.utilsService.handleLastIndex(this.total,this.projectsModel.data?.length||0,this.pageIndex,n=>{this.pageIndex=n,this.getProjects()})}searchProjects(){var t=this;return(0,ce.Z)(function*(){t.searchForm.value.search&&pt.s.track(Ve.KI),yield t.getProjects()})()}isOwnerOrAdmin(){return this.auth.isOwnerOrAdmin()}openProjectForm(t){pt.s.track(t?Ve.GN:Ve._0),this.projectsForm?.open(t,!!t)}selectProject(t,n){if(!t)return;let s="tasks-list";switch(n){case"TASK_LIST":default:s="tasks-list";break;case"BOARD":s="board"}this.router.navigate([`/worklenz/projects/${t}`],{queryParams:{tab:`${s}`,pinned_tab:`${s}`}})}getTaskProgressTitle(t){return t.all_tasks_count?t.all_tasks_count==t.completed_tasks_count?"All tasks completed.":`${t.completed_tasks_count||0}/${t.all_tasks_count||0} tasks completed.`:"No tasks available."}toggleFavorite(t){var n=this;return(0,ce.Z)(function*(){if(t)try{const s=yield n.api.toggleFavorite(t);1==n.filterIndex&&s.done&&n.getProjects(!0)}catch(s){(0,st.tu)(s)}})()}toggleArchive(t){var n=this;return(0,ce.Z)(function*(){if(t&&t.id&&!t.loading)try{t.loading=!0,n.isOwnerOrAdmin()?(yield n.api.toggleArchiveAll(t.id)).done&&(pt.s.track(t.archived?Ve.vE:Ve.S9),yield n.getProjects(!0)):(yield n.api.toggleArchive(t.id)).done&&(pt.s.track(t.archived?Ve.$n:Ve.z1),yield n.getProjects(!0)),t.loading=!1}catch(s){t.loading=!1,(0,st.tu)(s)}})()}onFilterChange(t){this.loading||(this.filterIndex=t,this.trackVisit(),this.getProjects())}trackBy(t,n){return n.id}trackVisit(){pt.s.track(Ve.QJ,{filter:this.getFilterIndexText()})}onProjectCreate(t){t?.id&&(this.list.setCurrentGroup(this.list.GROUP_BY_OPTIONS[0]),this.selectProject(t.id,"TASK_LIST"))}onQueryParamsChange(t){const{pageSize:n,pageIndex:s,sort:o}=t;this.pageIndex=s,this.pageSize=n;const h=o.find(p=>null!==p.value);this.sortField=h?.key??null,this.sortOrder=h?.value??null,this.getProjects()}filterByCategory(t){if(!t)return;const n=this.categories.find(s=>s.id===t);n&&(n.selected=!0,this.onCategoryFilterChange())}filterByStatus(t){if(!t)return;const n=this.statuses.find(s=>s.id===t);n&&(n.selected=!0,this.onStatusFilterChange())}onCategoryFilterChange(){const t=this.categories.filter(s=>s.selected);this.filteredByCategory=!!t.length;const n=t.map(s=>s.id).join("+");this.categoriesFilterString=n||null,this.getProjects()}onStatusFilterChange(){const t=this.statuses.filter(s=>s.selected);this.filteredByStatus=!!t.length;const n=t.map(s=>s.id).join("+");this.statusesFilterString=n||null,this.getProjects()}onProjectUpdated(){this.getProjects(),this.getCategories()}openTemplateSelector(){this.projectTemplateDrawer.open(),this.cdr.markForCheck(),pt.s.track(Ve.Ye)}}return(r=i).\u0275fac=function(t){return new(t||r)(e.Y36(ia.z),e.Y36(ji.U),e.Y36(Pe.qu),e.Y36(yn.e),e.Y36(wt.F0),e.Y36(wt.gz),e.Y36(JA.F),e.Y36(Bd.s),e.Y36(Lc.I),e.Y36(e.sBO),e.Y36(YA.b),e.Y36(tp.k))},r.\u0275cmp=e.Xpm({type:r,selectors:[["worklenz-projects"]],viewQuery:function(t,n){if(1&t&&(e.Gf(yr.g,5),e.Gf(Cd,5),e.Gf(Oc,7,e.Rgc),e.Gf(Ec.p,5)),2&t){let s;e.iGM(s=e.CRH())&&(n.projectsForm=s.first),e.iGM(s=e.CRH())&&(n.folderForm=s.first),e.iGM(s=e.CRH())&&(n.displayModeSegments=s.first),e.iGM(s=e.CRH())&&(n.projectTemplateDrawer=s.first)}},decls:51,vars:52,consts:[[1,"container"],[1,"px-0",3,"nzGhost"],["nz-button","","nz-tooltip","",3,"nzShape","nzTooltipTitle","nzType","click",4,"nzSpaceItem"],[3,"ngModel","nzOptions","ngModelChange","nzValueChange",4,"nzSpaceItem"],["nz-form","",3,"formGroup","nzLayout","ngSubmit",4,"nzSpaceItem"],[4,"ngIf"],[3,"nzActive","nzLoading","nzParagraph"],[1,"custom-table",3,"nzData","nzFrontPagination","nzLoading","nzPageIndex","nzPageSizeOptions","nzPageSize","nzTotal","nzShowSizeChanger","nzSize","nzNoResult","nzQueryParams"],["table",""],["scope","col",3,"nzSortFn","nzColumnKey"],["scope","col",3,"nzSortFn","nzColumnKey","nzFilterFn","nzCustomFilter"],[3,"nzVisible","nzActive","nzDropdownMenu","nzVisibleChange"],["nz-icon","",3,"nzType","nzTheme"],["scope","col",2,"width","110px",3,"nzSortFn","nzColumnKey","nzFilterFn","nzCustomFilter"],["scope","col",1,"pe-5","b-none"],["scope","col",3,"nzSortFn","nzColumnKey","nzAlign"],["colspan","2","scope","col",3,"nzAlign"],["class","actions-row cursor-pointer",3,"click",4,"ngFor","ngForOf","ngForTrackBy"],[3,"onCreate","onDelete","onUpdate"],[3,"showBothTabs","importProject"],["noDataTemplate",""],["categoriesDropdown","nzDropdownMenu"],["nz-menu","","nzSelectable",""],["nz-menu-item","","nzDisabled","",4,"ngIf"],["statusesDropdown","nzDropdownMenu"],["nz-button","","nz-tooltip","",3,"nzShape","nzTooltipTitle","nzType","click"],["nz-icon","",3,"nzSpin","nzTheme","nzType"],[3,"ngModel","nzOptions","ngModelChange","nzValueChange"],["nz-form","",3,"formGroup","nzLayout","ngSubmit"],[3,"nzSuffix"],["nz-input","","placeholder","Search by name","type","text",3,"formControlName"],["suffixIconSearch",""],["nz-icon","","nzType","search"],[4,"nzSpaceItem"],["menu","nzDropdownMenu"],["nz-menu",""],["nz-menu-item","",3,"click"],["nz-icon","","nzType","import","nzTheme","outline",1,"me-2"],["nz-button","",3,"nzType","click",4,"ngIf"],["nz-button","","nz-dropdown","",3,"nzType","nzDropdownMenu","nzTrigger","nzPlacement",4,"ngIf"],["nz-button","",3,"nzType","click"],["nz-icon","","nzType","edit","nzTheme","outline"],["nz-button","","nz-dropdown","",3,"nzType","nzDropdownMenu","nzTrigger","nzPlacement"],["nz-icon","","nzType","down","nzTheme","outline"],[1,"actions-row","cursor-pointer",3,"click"],[1,""],[1,"align-items-center","d-flex","w-100"],["nz-tooltip","",1,"me-2",3,"nzTooltipTitle","nzCount","ngModel","click"],[1,"w-100","project-name-col"],[3,"nzColor","nzSize"],[3,"ngSwitch"],["class","rounded-pill category-tag","nz-tooltip","",3,"nzTooltipTitle","nzColor","click",4,"ngSwitchCase"],[4,"ngSwitchCase"],["nz-icon","",3,"nzType"],["nz-tooltip","",3,"nzPercent","nzStrokeColor","nzTooltipTitle"],["nz-tooltip","",3,"nzTooltipTitle","nzTooltipMouseEnterDelay"],[4,"ngIf","ngIfElse"],["avatars",""],[3,"nzAlign"],["class","actions",4,"ngIf"],["nz-typography","","nz-tooltip","",1,"ms-1",3,"nzType","nzTooltipTitle"],["titleContent",""],["nz-tooltip","",1,"rounded-pill","category-tag",3,"nzTooltipTitle","nzColor","click"],[3,"names"],[1,"actions"],["nz-button","","nz-tooltip","",3,"nzSize","nzTooltipPlacement","nzTooltipTitle","nzType","click",4,"nzSpaceItem"],["nz-button","","nz-tooltip","",3,"nzSize","nzTooltipPlacement","nzTooltipTitle","nzLoading","nzType","click",4,"nzSpaceItem"],["nz-button","","nz-tooltip","",3,"nzSize","nzTooltipPlacement","nzTooltipTitle","nzType","click"],["nz-button","","nz-tooltip","",3,"nzSize","nzTooltipPlacement","nzTooltipTitle","nzLoading","nzType","click"],[1,"pt-5","pb-5"],[1,"no-data-img-holder","mx-auto","mb-4"],["src","/assets/images/empty-box.webp","alt","",1,"img-fluid"],["nz-typography","",1,"no-data-text",3,"ngSwitch"],["class","m-0 d-flex align-items-baseline","nz-checkbox","","nz-menu-item","",3,"nzChecked","nzCheckedChange",4,"ngFor","ngForOf"],["nz-checkbox","","nz-menu-item","",1,"m-0","d-flex","align-items-baseline",3,"nzChecked","nzCheckedChange"],["nz-menu-item","","nzDisabled",""]],template:function(t,n){if(1&t&&(e.TgZ(0,"div",0)(1,"nz-page-header",1)(2,"nz-page-header-title"),e._uU(3),e.qZA(),e.TgZ(4,"nz-page-header-extra")(5,"nz-space"),e.YNc(6,np,2,6,"button",2),e.YNc(7,co,1,2,"nz-segmented",3),e.YNc(8,sa,5,4,"form",4),e.YNc(9,Dc,8,0,"ng-container",5),e.qZA()()(),e.TgZ(10,"nz-card")(11,"nz-skeleton",6)(12,"nz-table",7,8),e.NdJ("nzQueryParams",function(o){return n.onQueryParamsChange(o)}),e.TgZ(14,"thead")(15,"tr")(16,"th",9),e._uU(17,"Name"),e.qZA(),e.TgZ(18,"th",9),e._uU(19,"Client"),e.qZA(),e.TgZ(20,"th",10),e._uU(21," Category "),e.TgZ(22,"nz-filter-trigger",11),e.NdJ("nzVisibleChange",function(o){return n.showCategoriesFilter=o}),e._UZ(23,"span",12),e.qZA()(),e.TgZ(24,"th",13),e._uU(25," Status "),e.TgZ(26,"nz-filter-trigger",11),e.NdJ("nzVisibleChange",function(o){return n.showStatusesFilter=o}),e._UZ(27,"span",12),e.qZA()(),e.TgZ(28,"th",14),e._uU(29,"Tasks Progress"),e.qZA(),e.TgZ(30,"th",15),e._uU(31,"Last Updated"),e.qZA(),e.TgZ(32,"th",16),e._uU(33,"Members"),e.qZA()()(),e.TgZ(34,"tbody"),e.YNc(35,Zc,30,30,"tr",17),e.qZA()()()()(),e.TgZ(36,"worklenz-project-form-modal",18),e.NdJ("onCreate",function(o){return n.onProjectCreate(o)})("onDelete",function(){return n.getProjects()})("onUpdate",function(){return n.onProjectUpdated()}),e.qZA(),e._UZ(37,"worklenz-projects-folder-form-drawer"),e.TgZ(38,"worklenz-project-template-import-drawer",19),e.NdJ("importProject",function(){return n.getProjects(!1)}),e.qZA(),e.YNc(39,Gc,7,4,"ng-template",null,20,e.W1O),e.TgZ(41,"nz-dropdown-menu",null,21)(43,"ul",22),e.YNc(44,la,2,1,"ng-container",5),e.YNc(45,Kc,2,0,"li",23),e.qZA()(),e.TgZ(46,"nz-dropdown-menu",null,24)(48,"ul",22),e.YNc(49,ip,2,1,"ng-container",5),e.YNc(50,sp,2,0,"li",23),e.qZA()()),2&t){const s=e.MAs(13),o=e.MAs(40),h=e.MAs(42),p=e.MAs(47);e.xp6(1),e.Q6J("nzGhost",!1),e.xp6(2),e.AsE("",n.total||0," Project",n.total>1?"s":"",""),e.xp6(6),e.Q6J("ngIf",n.isOwnerOrAdmin()),e.xp6(2),e.Q6J("nzActive",!0)("nzLoading",n.loading)("nzParagraph",e.DdM(50,kd)),e.xp6(1),e.Q6J("nzData",n.projectsModel.data||e.DdM(51,yd))("nzFrontPagination",!1)("nzLoading",n.loading)("nzPageIndex",n.pageIndex)("nzPageSizeOptions",n.paginationSizes)("nzPageSize",n.pageSize)("nzTotal",n.total)("nzShowSizeChanger",!0)("nzSize","small")("nzNoResult",o),e.xp6(4),e.Q6J("nzSortFn",!0)("nzColumnKey","name"),e.xp6(2),e.Q6J("nzSortFn",!0)("nzColumnKey","client_name"),e.xp6(2),e.Q6J("nzSortFn",!0)("nzColumnKey","category_id")("nzFilterFn",!0)("nzCustomFilter",!0),e.xp6(2),e.Q6J("nzVisible",n.showCategoriesFilter)("nzActive",n.filteredByCategory)("nzDropdownMenu",h),e.xp6(1),e.Q6J("nzType","filter")("nzTheme","fill"),e.xp6(1),e.Q6J("nzSortFn",!0)("nzColumnKey","status_id")("nzFilterFn",!0)("nzCustomFilter",!0),e.xp6(2),e.Q6J("nzVisible",n.showStatusesFilter)("nzActive",n.filteredByStatus)("nzDropdownMenu",p),e.xp6(1),e.Q6J("nzType","filter")("nzTheme","fill"),e.xp6(3),e.Q6J("nzSortFn",!0)("nzColumnKey","updated_at")("nzAlign","left"),e.xp6(2),e.Q6J("nzAlign","left"),e.xp6(3),e.Q6J("ngForOf",s.data)("ngForTrackBy",n.trackBy),e.xp6(3),e.Q6J("showBothTabs",!0),e.xp6(6),e.Q6J("ngIf",n.categories.length),e.xp6(1),e.Q6J("ngIf",!n.categories.length),e.xp6(4),e.Q6J("ngIf",n.statuses.length),e.xp6(1),e.Q6J("ngIf",!n.statuses.length)}},dependencies:[Ie.sg,Ie.O5,Ie.RF,Ie.n9,Pe._Y,Pe.Fj,Pe.JJ,Pe.JL,Pe.sg,Pe.u,mn.Lr,nA.Zp,nA.gB,nA.ke,Tr.$O,Tr.u9,Tr.Jp,Vt.ix,Vt.fY,Wt.w,an.dQ,xt.N8,xt.qD,xt.Uo,xt._C,xt.Om,xt.p0,xt.$Z,xt.Ql,xt.UX,zt.Ls,Ut.ZU,AA.j,Pe.On,qA.Ie,ms.M,Zt.SY,Mn.ng,DA.bd,HA.NU,HA.$1,fr.x7,Ri.wY,zn.wO,zn.r9,cn.cm,cn.RR,cn.wA,Nc.sn,FA.o,yr.g,Ec.p,Cd,Ie.uU,VA.m,bd],styles:[".actions-row[_ngcontent-%COMP%] td[_ngcontent-%COMP%]{padding:5px 8px!important}.project-name-col[_ngcontent-%COMP%]{display:flex;align-items:center}.no-data-img-holder[_ngcontent-%COMP%]{width:100px}.category-tag[_ngcontent-%COMP%]:hover{text-decoration:underline}"],changeDetection:0}),i})();var da=oe(5227),mA=oe(40581),qn=oe(62816);function vs(r,i){(0,qn.Z)(2,arguments);var A=(0,mA.Z)(r),t=(0,mA.Z)(i),n=A.getTime()-t.getTime();return n<0?-1:n>0?1:n}var Qd=oe(80275),Fd=oe(78970),zd=oe(13061),ap=oe(46719),Ud=oe(48292),Yc=oe(45351);function Id(r,i,A){var t,n;(0,qn.Z)(2,arguments);var s=(0,da.j)(),o=null!==(t=null!==(n=A?.locale)&&void 0!==n?n:s.locale)&&void 0!==t?t:ap.Z;if(!o.formatDistance)throw new RangeError("locale must contain formatDistance property");var h=vs(r,i);if(isNaN(h))throw new RangeError("Invalid time value");var _,B,p=(0,Ud.Z)(function Jc(r){return(0,Ud.Z)({},r)}(A),{addSuffix:!!A?.addSuffix,comparison:h});h>0?(_=(0,mA.Z)(i),B=(0,mA.Z)(r)):(_=(0,mA.Z)(r),B=(0,mA.Z)(i));var M,x=(0,zd.Z)(B,_),y=((0,Yc.Z)(B)-(0,Yc.Z)(_))/1e3,v=Math.round((x-y)/60);if(v<2)return null!=A&&A.includeSeconds?x<5?o.formatDistance("lessThanXSeconds",5,p):x<10?o.formatDistance("lessThanXSeconds",10,p):x<20?o.formatDistance("lessThanXSeconds",20,p):x<40?o.formatDistance("halfAMinute",0,p):o.formatDistance(x<60?"lessThanXMinutes":"xMinutes",1,p):0===v?o.formatDistance("lessThanXMinutes",1,p):o.formatDistance("xMinutes",v,p);if(v<45)return o.formatDistance("xMinutes",v,p);if(v<90)return o.formatDistance("aboutXHours",1,p);if(v<1440){var E=Math.round(v/60);return o.formatDistance("aboutXHours",E,p)}if(v<2520)return o.formatDistance("xDays",1,p);if(v<43200){var Z=Math.round(v/1440);return o.formatDistance("xDays",Z,p)}if(v<86400)return M=Math.round(v/43200),o.formatDistance("aboutXMonths",M,p);if(M=function op(r,i){(0,qn.Z)(2,arguments);var o,A=(0,mA.Z)(r),t=(0,mA.Z)(i),n=vs(A,t),s=Math.abs((0,Qd.Z)(A,t));if(s<1)o=0;else{1===A.getMonth()&&A.getDate()>27&&A.setDate(30),A.setMonth(A.getMonth()-n*s);var h=vs(A,t)===-n;(0,Fd.Z)((0,mA.Z)(r))&&1===s&&1===vs(r,t)&&(h=!1),o=n*(s-Number(h))}return 0===o?0:o}(B,_),M<12){var I=Math.round(v/43200);return o.formatDistance("xMonths",I,p)}var j=M%12,X=Math.floor(M/12);return j<3?o.formatDistance("aboutXYears",X,p):j<9?o.formatDistance("overXYears",X,p):o.formatDistance("almostXYears",X+1,p)}var en=oe(71002),tn=Uint8Array,Vn=Uint16Array,WA=Uint32Array,Bs=new tn([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),bs=new tn([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),Ci=new tn([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),xs=function(r,i){for(var A=new Vn(31),t=0;t<31;++t)A[t]=i+=1<>>1|(21845&Dt)<<1;_o[Dt]=((65280&(Bi=(61680&(Bi=(52428&Bi)>>>2|(13107&Bi)<<2))>>>4|(3855&Bi)<<4))>>>8|(255&Bi)<<8)>>>1}var XA=function(r,i,A){for(var t=r.length,n=0,s=new Vn(i);n>>p]=_}else for(h=new Vn(t),n=0;n>>15-r[n];return h},Yr=new tn(288);for(Dt=0;Dt<144;++Dt)Yr[Dt]=8;for(Dt=144;Dt<256;++Dt)Yr[Dt]=9;for(Dt=256;Dt<280;++Dt)Yr[Dt]=7;for(Dt=280;Dt<288;++Dt)Yr[Dt]=8;var ys=new tn(32);for(Dt=0;Dt<32;++Dt)ys[Dt]=5;var Ld=XA(Yr,9,0),Nd=XA(Yr,9,1),Od=XA(ys,5,0),Pd=XA(ys,5,1),Ji=function(r){for(var i=r[0],A=1;Ai&&(i=r[A]);return i},$A=function(r,i,A){var t=i/8>>0;return(r[t]|r[t+1]<<8)>>>(7&i)&A},wo=function(r,i){var A=i/8>>0;return(r[A]|r[A+1]<<8|r[A+2]<<16)>>>(7&i)},vo=function(r){return(r/8>>0)+(7&r&&1)},Yi=function(r,i,A){(null==i||i<0)&&(i=0),(null==A||A>r.length)&&(A=r.length);var t=new(r instanceof Vn?Vn:r instanceof WA?WA:tn)(A-i);return t.set(r.subarray(i,A)),t},er=function(r,i,A){var t=i/8>>0;r[t]|=A<<=7&i,r[t+1]|=A>>>8},tr=function(r,i,A){var t=i/8>>0;r[t]|=A<<=7&i,r[t+1]|=A>>>8,r[t+2]|=A>>>16},ha=function(r,i){for(var A=[],t=0;ty&&(y=s[t].s);var v=new Vn(y+1),M=pa(A[B-1],v,0);if(M>i){t=0;var E=0,Z=M-i,I=1<i))break;E+=I-(1<>>=Z;E>0;){var X=s[t].s;v[X]=0&&E;--t){var ie=s[t].s;v[ie]==i&&(--v[ie],++E)}M=i}return[new tn(v),M]},pa=function(r,i,A){return-1==r.s?Math.max(pa(r.l,i,A+1),pa(r.r,i,A+1)):i[r.s]=A},Wc=function(r){for(var i=r.length;i&&!r[--i];);for(var A=new Vn(++i),t=0,n=r[0],s=1,o=function(p){A[t++]=p},h=1;h<=i;++h)if(r[h]==n&&h!=i)++s;else{if(!n&&s>2){for(;s>138;s-=138)o(32754);s>2&&(o(s>10?s-11<<5|28690:s-3<<5|12305),s=0)}else if(s>3){for(o(n),--s;s>6;s-=6)o(8304);s>2&&(o(s-3<<5|8208),s=0)}for(;s--;)o(n);s=1,n=r[h]}return[A.subarray(0,t),i]},kr=function(r,i){for(var A=0,t=0;t>>8,r[n+2]=255^r[n],r[n+3]=255^r[n+1];for(var s=0;s4&&!Fe[Ci[P-1]];--P);var ve,ke,fe,Ue,$=_+5<<3,q=kr(n,Yr)+kr(s,ys)+o,be=kr(n,y)+kr(s,E)+o+14+3*P+kr(ue,Fe)+(2*ue[16]+3*ue[17]+7*ue[18]);if($<=q&&$<=be)return Bo(i,B,r.subarray(p,p+_));if(er(i,B,1+(be15&&(er(i,B,G[Y]>>>5&127),B+=G[Y]>>>12)}}else ve=Ld,ke=Yr,fe=Od,Ue=ys;for(Y=0;Y255){var K;tr(i,B,ve[257+(K=t[Y]>>>18&31)]),B+=ke[K+257],K>7&&(er(i,B,t[Y]>>>23&31),B+=Bs[K]);var ne=31&t[Y];tr(i,B,fe[ne]),B+=Ue[ne],ne>3&&(tr(i,B,t[Y]>>>5&8191),B+=bs[ne])}else tr(i,B,ve[t[Y]]),B+=ke[t[Y]];return tr(i,B,ve[256]),B+ke[256]},Dd=new WA([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),Hd=new tn(0),bo=function(r,i,A,t,n){return function(r,i,A,t,n,s){var o=r.length,h=new tn(t+o+5*(1+Math.floor(o/7e3))+n),p=h.subarray(t,h.length-n),_=0;if(!i||o<8)for(var B=0;B<=o;B+=65535){var x=B+65535;x>>13,M=8191&y,E=(1<7e3||Fe>24576)&&ve>423){_=Xc(r,p,0,_e,Se,ue,Qe,Fe,P,B-P,_),Fe=Y=Qe=0,P=B;for(var ke=0;ke<286;++ke)Se[ke]=0;for(ke=0;ke<30;++ke)ue[ke]=0}var fe=2,Ue=0,ze=M,et=q-be&32767;if(ve>2&&$==ie(B-et))for(var U=Math.min(v,ve)-1,G=Math.min(32767,B),K=Math.min(258,ve);et<=G&&--ze&&q!=be;){if(r[B+fe]==r[B+fe-et]){for(var ne=0;nefe){if(fe=ne,Ue=et,ne>U)break;var ae=Math.min(et,ne-2),ge=0;for(ke=0;kege&&(ge=Re,be=we)}}}et+=(q=be)-(be=Z[q])+32768&32767}if(Ue){_e[Fe++]=268435456|ua[fe]<<18|Ki[Ue];var je=31&ua[fe],rt=31&Ki[Ue];Qe+=Bs[je]+bs[rt],++Se[257+je],++ue[rt],D=B+fe,++Y}else _e[Fe++]=r[B],++Se[r[B]]}}_=Xc(r,p,s,_e,Se,ue,Qe,Fe,P,B-P,_),s||(_=Bo(p,_,Hd))}return Yi(h,0,t+vo(_)+n)}(r,null==i.level?6:i.level,null==i.mem?Math.ceil(1.5*Math.max(8,Math.min(13,Math.log(r.length)))):12+i.mem,A,t,!n)};function ks(r,i){void 0===i&&(i={});var A=function(){var r=1,i=0;return{p:function(A){for(var t=r,n=i,s=A.length,o=0;o!=s;){for(var h=Math.min(o+5552,s);o>>8<<16|(255&i)<<8|i>>>8)+2*((255&r)<<23)}}}();A.p(r);var t=bo(r,i,2,4);return function(r,i){var A=i.level,t=0==A?0:A<6?1:9==A?3:2;r[0]=120,r[1]=t<<6|(t?32-2*t:1)}(t,i),function(r,i,A){for(;A;++i)r[i]=A,A>>>=8}(t,t.length-4,A.d()),t}function nn(r,i){return function(r,i,A){var t=r.length,n=!i||A,s=!A||A.i;A||(A={}),i||(i=new tn(3*t));var o=function(ae){var ge=i.length;if(ae>ge){var we=new tn(Math.max(2*ge,ae));we.set(i),i=we}},h=A.f||0,p=A.p||0,_=A.b||0,B=A.l,x=A.d,y=A.m,v=A.n,M=8*t;do{if(!B){A.f=h=$A(r,p,1);var E=$A(r,p+1,3);if(p+=3,!E){var I=r[(Z=vo(p)+4)-4]|r[Z-3]<<8,j=Z+I;if(j>t){if(s)throw"unexpected EOF";break}n&&o(_+I),i.set(r.subarray(Z,j),_),A.b=_+=I,A.p=p=8*j;continue}if(1==E)B=Nd,x=Pd,y=9,v=5;else{if(2!=E)throw"invalid block type";var X=$A(r,p,31)+257,ie=$A(r,p+10,15)+4,_e=X+$A(r,p+5,31)+1;p+=14;for(var Se=new tn(_e),ue=new tn(19),Y=0;YM)break;var D=XA(ue,Qe,1);for(Y=0;Y<_e;){var Z,P=D[$A(r,p,Fe)];if(p+=15&P,(Z=P>>>4)<16)Se[Y++]=Z;else{var $=0,q=0;for(16==Z?(q=3+$A(r,p,3),p+=2,$=Se[Y-1]):17==Z?(q=3+$A(r,p,7),p+=3):18==Z&&(q=11+$A(r,p,127),p+=7);q--;)Se[Y++]=$}}var be=Se.subarray(0,X),ve=Se.subarray(X);y=Ji(be),v=Ji(ve),B=XA(be,y,1),x=XA(ve,v,1)}if(p>M)throw"unexpected EOF"}n&&o(_+131072);for(var ke=(1<>>4;if((p+=15&$)>M)throw"unexpected EOF";if(!$)throw"invalid length/literal";if(ze<256)i[_++]=ze;else{if(256==ze){B=null;break}var et=ze-254;ze>264&&(et=$A(r,p,(1<<(U=Bs[Y=ze-257]))-1)+Vc[Y],p+=U);var G=x[wo(r,p)&fe],K=G>>>4;if(!G)throw"invalid distance";if(p+=15&G,ve=mo[K],K>3){var U=bs[K];ve+=wo(r,p)&(1<M)throw"unexpected EOF";n&&o(_+131072);for(var ne=_+et;_>>4>7||(r[0]<<8|r[1])%31)throw"invalid zlib data";if(32&r[1])throw"invalid zlib data: preset dictionaries not supported"}(r),r.subarray(2,-4)),i)}var mt=function(){return typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this}();function Ba(){mt.console&&"function"==typeof mt.console.log&&mt.console.log.apply(mt.console,arguments)}var sn={log:Ba,warn:function(r){mt.console&&("function"==typeof mt.console.warn?mt.console.warn.apply(mt.console,arguments):Ba.call(null,arguments))},error:function(r){mt.console&&("function"==typeof mt.console.error?mt.console.error.apply(mt.console,arguments):Ba(r))}};function ba(r,i,A){var t=new XMLHttpRequest;t.open("GET",r),t.responseType="blob",t.onload=function(){Tn(t.response,i,A)},t.onerror=function(){sn.error("could not download file")},t.send()}function sl(r){var i=new XMLHttpRequest;i.open("HEAD",r,!1);try{i.send()}catch{}return i.status>=200&&i.status<=299}function xA(r){try{r.dispatchEvent(new MouseEvent("click"))}catch{var i=document.createEvent("MouseEvents");i.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),r.dispatchEvent(i)}}var Ar,Qr,Tn=mt.saveAs||("object"!==(typeof window>"u"?"undefined":(0,en.Z)(window))||window!==mt?function(){}:typeof HTMLAnchorElement<"u"&&"download"in HTMLAnchorElement.prototype?function(r,i,A){var t=mt.URL||mt.webkitURL,n=document.createElement("a");n.download=i=i||r.name||"download",n.rel="noopener","string"==typeof r?(n.href=r,n.origin!==location.origin?sl(n.href)?ba(r,i,A):xA(n,n.target="_blank"):xA(n)):(n.href=t.createObjectURL(r),setTimeout(function(){t.revokeObjectURL(n.href)},4e4),setTimeout(function(){xA(n)},0))}:"msSaveOrOpenBlob"in navigator?function(r,i,A){if(i=i||r.name||"download","string"==typeof r)if(sl(r))ba(r,i,A);else{var t=document.createElement("a");t.href=r,t.target="_blank",setTimeout(function(){xA(t)})}else navigator.msSaveOrOpenBlob((n=r,void 0===(s=A)?s={autoBom:!1}:"object"!==(0,en.Z)(s)&&(sn.warn("Deprecated: Expected third argument to be a object"),s={autoBom:!s}),s.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(n.type)?new Blob([String.fromCharCode(65279),n],{type:n.type}):n),i);var n,s}:function(r,i,A,t){if((t=t||open("","_blank"))&&(t.document.title=t.document.body.innerText="downloading..."),"string"==typeof r)return ba(r,i,A);var n="application/octet-stream"===r.type,s=/constructor/i.test(mt.HTMLElement)||mt.safari,o=/CriOS\/[\d]+/.test(navigator.userAgent);if((o||n&&s)&&"object"===(typeof FileReader>"u"?"undefined":(0,en.Z)(FileReader))){var h=new FileReader;h.onloadend=function(){var B=h.result;B=o?B:B.replace(/^data:[^;]*;/,"data:attachment/file;"),t?t.location.href=B:location=B,t=null},h.readAsDataURL(r)}else{var p=mt.URL||mt.webkitURL,_=p.createObjectURL(r);t?t.location=_:location.href=_,t=null,setTimeout(function(){p.revokeObjectURL(_)},4e4)}});function Wr(r){var i;r=r||"",this.ok=!1,"#"==r.charAt(0)&&(r=r.substr(1,6)),r={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"}[r=(r=r.replace(/ /g,"")).toLowerCase()]||r;for(var A=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(h){return[parseInt(h[1]),parseInt(h[2]),parseInt(h[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(h){return[parseInt(h[1],16),parseInt(h[2],16),parseInt(h[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(h){return[parseInt(h[1]+h[1],16),parseInt(h[2]+h[2],16),parseInt(h[3]+h[3],16)]}}],t=0;t255?255:this.r,this.g=this.g<0||isNaN(this.g)?0:this.g>255?255:this.g,this.b=this.b<0||isNaN(this.b)?0:this.b>255?255:this.b,this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"},this.toHex=function(){var h=this.r.toString(16),p=this.g.toString(16),_=this.b.toString(16);return 1==h.length&&(h="0"+h),1==p.length&&(p="0"+p),1==_.length&&(_="0"+_),"#"+h+p+_}}function xa(r,i){var A=r[0],t=r[1],n=r[2],s=r[3];A=rA(A,t,n,s,i[0],7,-680876936),s=rA(s,A,t,n,i[1],12,-389564586),n=rA(n,s,A,t,i[2],17,606105819),t=rA(t,n,s,A,i[3],22,-1044525330),A=rA(A,t,n,s,i[4],7,-176418897),s=rA(s,A,t,n,i[5],12,1200080426),n=rA(n,s,A,t,i[6],17,-1473231341),t=rA(t,n,s,A,i[7],22,-45705983),A=rA(A,t,n,s,i[8],7,1770035416),s=rA(s,A,t,n,i[9],12,-1958414417),n=rA(n,s,A,t,i[10],17,-42063),t=rA(t,n,s,A,i[11],22,-1990404162),A=rA(A,t,n,s,i[12],7,1804603682),s=rA(s,A,t,n,i[13],12,-40341101),n=rA(n,s,A,t,i[14],17,-1502002290),A=uA(A,t=rA(t,n,s,A,i[15],22,1236535329),n,s,i[1],5,-165796510),s=uA(s,A,t,n,i[6],9,-1069501632),n=uA(n,s,A,t,i[11],14,643717713),t=uA(t,n,s,A,i[0],20,-373897302),A=uA(A,t,n,s,i[5],5,-701558691),s=uA(s,A,t,n,i[10],9,38016083),n=uA(n,s,A,t,i[15],14,-660478335),t=uA(t,n,s,A,i[4],20,-405537848),A=uA(A,t,n,s,i[9],5,568446438),s=uA(s,A,t,n,i[14],9,-1019803690),n=uA(n,s,A,t,i[3],14,-187363961),t=uA(t,n,s,A,i[8],20,1163531501),A=uA(A,t,n,s,i[13],5,-1444681467),s=uA(s,A,t,n,i[2],9,-51403784),n=uA(n,s,A,t,i[7],14,1735328473),A=wA(A,t=uA(t,n,s,A,i[12],20,-1926607734),n,s,i[5],4,-378558),s=wA(s,A,t,n,i[8],11,-2022574463),n=wA(n,s,A,t,i[11],16,1839030562),t=wA(t,n,s,A,i[14],23,-35309556),A=wA(A,t,n,s,i[1],4,-1530992060),s=wA(s,A,t,n,i[4],11,1272893353),n=wA(n,s,A,t,i[7],16,-155497632),t=wA(t,n,s,A,i[10],23,-1094730640),A=wA(A,t,n,s,i[13],4,681279174),s=wA(s,A,t,n,i[0],11,-358537222),n=wA(n,s,A,t,i[3],16,-722521979),t=wA(t,n,s,A,i[6],23,76029189),A=wA(A,t,n,s,i[9],4,-640364487),s=wA(s,A,t,n,i[12],11,-421815835),n=wA(n,s,A,t,i[15],16,530742520),A=Zn(A,t=wA(t,n,s,A,i[2],23,-995338651),n,s,i[0],6,-198630844),s=Zn(s,A,t,n,i[7],10,1126891415),n=Zn(n,s,A,t,i[14],15,-1416354905),t=Zn(t,n,s,A,i[5],21,-57434055),A=Zn(A,t,n,s,i[12],6,1700485571),s=Zn(s,A,t,n,i[3],10,-1894986606),n=Zn(n,s,A,t,i[10],15,-1051523),t=Zn(t,n,s,A,i[1],21,-2054922799),A=Zn(A,t,n,s,i[8],6,1873313359),s=Zn(s,A,t,n,i[15],10,-30611744),n=Zn(n,s,A,t,i[6],15,-1560198380),t=Zn(t,n,s,A,i[13],21,1309151649),A=Zn(A,t,n,s,i[4],6,-145523070),s=Zn(s,A,t,n,i[11],10,-1120210379),n=Zn(n,s,A,t,i[2],15,718787259),t=Zn(t,n,s,A,i[9],21,-343485551),r[0]=iA(A,r[0]),r[1]=iA(t,r[1]),r[2]=iA(n,r[2]),r[3]=iA(s,r[3])}function ko(r,i,A,t,n,s){return i=iA(iA(i,r),iA(t,s)),iA(i<>>32-n,A)}function rA(r,i,A,t,n,s,o){return ko(i&A|~i&t,r,i,n,s,o)}function uA(r,i,A,t,n,s,o){return ko(i&t|A&~t,r,i,n,s,o)}function wA(r,i,A,t,n,s,o){return ko(i^A^t,r,i,n,s,o)}function Zn(r,i,A,t,n,s,o){return ko(A^(i|~t),r,i,n,s,o)}function Fr(r){var i,A=r.length,t=[1732584193,-271733879,-1732584194,271733878];for(i=64;i<=r.length;i+=64)xa(t,Cp(r.substring(i-64,i)));r=r.substring(i-64);var n=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i>2]|=r.charCodeAt(i)<<(i%4<<3);if(n[i>>2]|=128<<(i%4<<3),i>55)for(xa(t,n),i=0;i<16;i++)n[i]=0;return n[14]=8*A,xa(t,n),t}function Cp(r){var i,A=[];for(i=0;i<64;i+=4)A[i>>2]=r.charCodeAt(i)+(r.charCodeAt(i+1)<<8)+(r.charCodeAt(i+2)<<16)+(r.charCodeAt(i+3)<<24);return A}Ar=mt.atob.bind(mt),Qr=mt.btoa.bind(mt);var $d="0123456789abcdef".split("");function ya(r){for(var i="",A=0;A<4;A++)i+=$d[r>>8*A+4&15]+$d[r>>8*A&15];return i}function eu(r){return String.fromCharCode((255&r)>>0,(65280&r)>>8,(16711680&r)>>16,(4278190080&r)>>24)}function ol(r){return Fr(r).map(eu).join("")}var Bp="5d41402abc4b2a76b9719d911017c592"!=function(r){for(var i=0;i>16)+(i>>16)+(A>>16)<<16|65535&A}return r+i&4294967295}function Ta(r,i){var A,t,n;if(r!==A){for(var o=(n=r,new Array(1+(256/r.length>>0)+1).join(n)),h=[],p=0;p<256;p++)h[p]=p;var _=0;for(p=0;p<256;p++){var B=h[p];_=(_+B+o.charCodeAt(p))%256,h[p]=h[_],h[_]=B}A=r,t=h}else h=t;var x=i.length,y=0,v=0,M="";for(p=0;p\x80/\f\xa9\xfedSiz";var s=(i+this.padding).substr(0,32),o=(A+this.padding).substr(0,32);this.O=this.processOwnerPassword(s,o),this.P=-(1+(255^n)),this.encryptionKey=ol(s+this.O+this.lsbFirstWord(this.P)+this.hexToBytes(t)).substr(0,5),this.U=Ta(this.encryptionKey,this.padding)}function Xr(r){if(/[^\u0000-\u00ff]/.test(r))throw new Error("Invalid PDF Name Object: "+r+", Only accept ASCII characters.");for(var i="",A=r.length,t=0;t126?"#"+("0"+n.toString(16)).slice(-2):r[t]}return i}function nu(r){if("object"!==(0,en.Z)(r))throw new Error("Invalid Context passed to initialize PubSub (jsPDF-module)");var i={};this.subscribe=function(A,t,n){if(n=n||!1,"string"!=typeof A||"function"!=typeof t||"boolean"!=typeof n)throw new Error("Invalid arguments passed to PubSub.subscribe (jsPDF-module)");i.hasOwnProperty(A)||(i[A]={});var s=Math.random().toString(35);return i[A][s]=[t,!!n],s},this.unsubscribe=function(A){for(var t in i)if(i[t][A])return delete i[t][A],0===Object.keys(i[t]).length&&delete i[t],!0;return!1},this.publish=function(A){if(i.hasOwnProperty(A)){var t=Array.prototype.slice.call(arguments,1),n=[];for(var s in i[A]){var o=i[A][s];try{o[0].apply(r,t)}catch(h){mt.console&&sn.error("jsPDF PubSub Error",h.message,h)}o[1]&&n.push(s)}n.length&&n.forEach(this.unsubscribe)}},this.getTopics=function(){return i}}function Qo(r){if(!(this instanceof Qo))return new Qo(r);var i="opacity,stroke-opacity".split(",");for(var A in r)r.hasOwnProperty(A)&&i.indexOf(A)>=0&&(this[A]=r[A]);this.id="",this.objectNumber=-1}function al(r,i){this.gState=r,this.matrix=i,this.id="",this.objectNumber=-1}function xi(r,i,A,t,n){if(!(this instanceof xi))return new xi(r,i,A,t,n);this.type="axial"===r?2:3,this.coords=i,this.colors=A,al.call(this,t,n)}function qi(r,i,A,t,n){if(!(this instanceof qi))return new qi(r,i,A,t,n);this.boundingBox=r,this.xStep=i,this.yStep=A,this.stream="",this.cloneIndex=0,al.call(this,t,n)}function gt(r){var i,A="string"==typeof arguments[0]?arguments[0]:"p",t=arguments[1],n=arguments[2],s=arguments[3],o=[],h=1,p=16,_="S",B=null;"object"===(0,en.Z)(r=r||{})&&(A=r.orientation,t=r.unit||t,n=r.format||n,s=r.compress||r.compressPdf||s,null!==(B=r.encryption||null)&&(B.userPassword=B.userPassword||"",B.ownerPassword=B.ownerPassword||"",B.userPermissions=B.userPermissions||[]),h="number"==typeof r.userUnit?Math.abs(r.userUnit):1,void 0!==r.precision&&(i=r.precision),void 0!==r.floatPrecision&&(p=r.floatPrecision),_=r.defaultPathOperation||"S"),o=r.filters||(!0===s?["FlateEncode"]:o),t=t||"mm",A=(""+(A||"P")).toLowerCase();var x=r.putOnlyUsedFonts||!1,y={},v={internal:{},__private__:{}};v.__private__.PubSub=nu;var M="1.3",E=v.__private__.getPdfVersion=function(){return M};v.__private__.setPdfVersion=function(g){M=g};var Z={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};v.__private__.getPageFormats=function(){return Z};var I=v.__private__.getPageFormat=function(g){return Z[g]};n=n||"a4";var X="compat";function ie(){this.saveGraphicsState(),J(new vt(At,0,0,-At,0,qs()*At).toString()+" cm"),this.setFontSize(this.getFontSize()/At),_="n",X="advanced"}function _e(){this.restoreGraphicsState(),_="S",X="compat"}var Se=v.__private__.combineFontStyleAndFontWeight=function(g,k){if("bold"==g&&"normal"==k||"bold"==g&&400==k||"normal"==g&&"italic"==k||"bold"==g&&"italic"==k)throw new Error("Invalid Combination of fontweight and fontstyle");return k&&(g=400==k||"normal"===k?"italic"===g?"italic":"normal":700!=k&&"bold"!==k||"normal"!==g?(700==k?"bold":k)+""+g:"bold"),g};v.advancedAPI=function(g){var k="compat"===X;return k&&ie.call(this),"function"!=typeof g||(g(this),k&&_e.call(this)),this},v.compatAPI=function(g){var k="advanced"===X;return k&&_e.call(this),"function"!=typeof g||(g(this),k&&ie.call(this)),this},v.isAdvancedAPI=function(){return"advanced"===X};var ue,Y=function(g){if("advanced"!==X)throw new Error(g+" is only available in 'advanced' API mode. You need to call advancedAPI() first.")},Qe=v.roundToPrecision=v.__private__.roundToPrecision=function(g,k){var R=i||k;if(isNaN(g)||isNaN(R))throw new Error("Invalid argument passed to jsPDF.roundToPrecision");return g.toFixed(R).replace(/0+$/,"")};ue=v.hpf=v.__private__.hpf="number"==typeof p?function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.hpf");return Qe(g,p)}:"smart"===p?function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.hpf");return Qe(g,g>-1&&g<1?16:5)}:function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.hpf");return Qe(g,16)};var Fe=v.f2=v.__private__.f2=function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.f2");return Qe(g,2)},D=v.__private__.f3=function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.f3");return Qe(g,3)},P=v.scale=v.__private__.scale=function(g){if(isNaN(g))throw new Error("Invalid argument passed to jsPDF.scale");return"compat"===X?g*At:"advanced"===X?g:void 0},q=function(g){return P(function(g){return"compat"===X?qs()-g:"advanced"===X?g:void 0}(g))};v.__private__.setPrecision=v.setPrecision=function(g){"number"==typeof parseInt(g,10)&&(i=parseInt(g,10))};var be,ve="00000000000000000000000000000000",ke=v.__private__.getFileId=function(){return ve},fe=v.__private__.setFileId=function(g){return ve=void 0!==g&&/^[a-fA-F0-9]{32}$/.test(g)?g.toUpperCase():ve.split("").map(function(){return"ABCDEF0123456789".charAt(Math.floor(16*Math.random()))}).join(""),null!==B&&(OA=new zs(B.userPermissions,B.userPassword,B.ownerPassword,ve)),ve};v.setFileId=function(g){return fe(g),this},v.getFileId=function(){return ke()};var Ue=v.__private__.convertDateToPDFDate=function(g){var k=g.getTimezoneOffset(),R=k<0?"+":"-",V=Math.floor(Math.abs(k/60)),le=Math.abs(k%60),ye=[R,K(V),"'",K(le),"'"].join("");return["D:",g.getFullYear(),K(g.getMonth()+1),K(g.getDate()),K(g.getHours()),K(g.getMinutes()),K(g.getSeconds()),ye].join("")},ze=v.__private__.convertPDFDateToDate=function(g){var k=parseInt(g.substr(2,4),10),R=parseInt(g.substr(6,2),10)-1,V=parseInt(g.substr(8,2),10),le=parseInt(g.substr(10,2),10),ye=parseInt(g.substr(12,2),10),De=parseInt(g.substr(14,2),10);return new Date(k,R,V,le,ye,De,0)},et=v.__private__.setCreationDate=function(g){var k;if(void 0===g&&(g=new Date),g instanceof Date)k=Ue(g);else{if(!/^D:(20[0-2][0-9]|203[0-7]|19[7-9][0-9])(0[0-9]|1[0-2])([0-2][0-9]|3[0-1])(0[0-9]|1[0-9]|2[0-3])(0[0-9]|[1-5][0-9])(0[0-9]|[1-5][0-9])(\+0[0-9]|\+1[0-4]|-0[0-9]|-1[0-1])'(0[0-9]|[1-5][0-9])'?$/.test(g))throw new Error("Invalid argument passed to jsPDF.setCreationDate");k=g}return be=k},U=v.__private__.getCreationDate=function(g){var k=be;return"jsDate"===g&&(k=ze(be)),k};v.setCreationDate=function(g){return et(g),this},v.getCreationDate=function(g){return U(g)};var G,K=v.__private__.padd2=function(g){return("0"+parseInt(g)).slice(-2)},ne=v.__private__.padd2Hex=function(g){return("00"+(g=g.toString())).substr(g.length)},ae=0,ge=[],we=[],Ce=0,Re=[],je=[],rt=!1,at=we;v.__private__.setCustomOutputDestination=function(g){rt=!0,at=g};var Te=function(g){rt||(at=g)};v.__private__.resetCustomOutputDestination=function(){rt=!1,at=we};var J=v.__private__.out=function(g){return g=g.toString(),Ce+=g.length+1,at.push(g),at},It=v.__private__.write=function(g){return J(1===arguments.length?g.toString():Array.prototype.join.call(arguments," "))},ut=v.__private__.getArrayBuffer=function(g){for(var k=g.length,R=new ArrayBuffer(k),V=new Uint8Array(R);k--;)V[k]=g.charCodeAt(k);return R},He=[["Helvetica","helvetica","normal","WinAnsiEncoding"],["Helvetica-Bold","helvetica","bold","WinAnsiEncoding"],["Helvetica-Oblique","helvetica","italic","WinAnsiEncoding"],["Helvetica-BoldOblique","helvetica","bolditalic","WinAnsiEncoding"],["Courier","courier","normal","WinAnsiEncoding"],["Courier-Bold","courier","bold","WinAnsiEncoding"],["Courier-Oblique","courier","italic","WinAnsiEncoding"],["Courier-BoldOblique","courier","bolditalic","WinAnsiEncoding"],["Times-Roman","times","normal","WinAnsiEncoding"],["Times-Bold","times","bold","WinAnsiEncoding"],["Times-Italic","times","italic","WinAnsiEncoding"],["Times-BoldItalic","times","bolditalic","WinAnsiEncoding"],["ZapfDingbats","zapfdingbats","normal",null],["Symbol","symbol","normal",null]];v.__private__.getStandardFonts=function(){return He};var Ge=r.fontSize||16;v.__private__.setFontSize=v.setFontSize=function(g){return Ge="advanced"===X?g/At:g,this};var nt,qe=v.__private__.getFontSize=v.getFontSize=function(){return"compat"===X?Ge:Ge*At},ht=r.R2L||!1;v.__private__.setR2L=v.setR2L=function(g){return ht=g,this},v.__private__.getR2L=v.getR2L=function(){return ht};var Qt,Nt=v.__private__.setZoomMode=function(g){if(/^(?:\d+\.\d*|\d*\.\d+|\d+)%$/.test(g))nt=g;else if(isNaN(g)){if(-1===[void 0,null,"fullwidth","fullheight","fullpage","original"].indexOf(g))throw new Error('zoom must be Integer (e.g. 2), a percentage Value (e.g. 300%) or fullwidth, fullheight, fullpage, original. "'+g+'" is not recognized.');nt=g}else nt=parseInt(g,10)};v.__private__.getZoomMode=function(){return nt};var Rt,An=v.__private__.setPageMode=function(g){if(-1==[void 0,null,"UseNone","UseOutlines","UseThumbs","FullScreen"].indexOf(g))throw new Error('Page mode must be one of UseNone, UseOutlines, UseThumbs, or FullScreen. "'+g+'" is not recognized.');Qt=g};v.__private__.getPageMode=function(){return Qt};var vn=v.__private__.setLayoutMode=function(g){if(-1==[void 0,null,"continuous","single","twoleft","tworight","two"].indexOf(g))throw new Error('Layout mode must be one of continuous, single, twoleft, tworight. "'+g+'" is not recognized.');Rt=g};v.__private__.getLayoutMode=function(){return Rt},v.__private__.setDisplayMode=v.setDisplayMode=function(g,k,R){return Nt(g),vn(k),An(R),this};var Tt={title:"",subject:"",author:"",keywords:"",creator:""};v.__private__.getDocumentProperty=function(g){if(-1===Object.keys(Tt).indexOf(g))throw new Error("Invalid argument passed to jsPDF.getDocumentProperty");return Tt[g]},v.__private__.getDocumentProperties=function(){return Tt},v.__private__.setDocumentProperties=v.setProperties=v.setDocumentProperties=function(g){for(var k in Tt)Tt.hasOwnProperty(k)&&g[k]&&(Tt[k]=g[k]);return this},v.__private__.setDocumentProperty=function(g,k){if(-1===Object.keys(Tt).indexOf(g))throw new Error("Invalid arguments passed to jsPDF.setDocumentProperty");return Tt[g]=k};var Gt,At,NA,on,si,bn={},In={},Mi=[],fn={},Yo={},Gn={},oi={},is=null,Kn=0,Ft=[],gn=new nu(v),qo=r.hotfixes||[],TA={},Ei={},Li=[],vt=function g(k,R,V,le,ye,De){if(!(this instanceof g))return new g(k,R,V,le,ye,De);isNaN(k)&&(k=1),isNaN(R)&&(R=0),isNaN(V)&&(V=0),isNaN(le)&&(le=1),isNaN(ye)&&(ye=0),isNaN(De)&&(De=0),this._matrix=[k,R,V,le,ye,De]};Object.defineProperty(vt.prototype,"sx",{get:function(){return this._matrix[0]},set:function(g){this._matrix[0]=g}}),Object.defineProperty(vt.prototype,"shy",{get:function(){return this._matrix[1]},set:function(g){this._matrix[1]=g}}),Object.defineProperty(vt.prototype,"shx",{get:function(){return this._matrix[2]},set:function(g){this._matrix[2]=g}}),Object.defineProperty(vt.prototype,"sy",{get:function(){return this._matrix[3]},set:function(g){this._matrix[3]=g}}),Object.defineProperty(vt.prototype,"tx",{get:function(){return this._matrix[4]},set:function(g){this._matrix[4]=g}}),Object.defineProperty(vt.prototype,"ty",{get:function(){return this._matrix[5]},set:function(g){this._matrix[5]=g}}),Object.defineProperty(vt.prototype,"a",{get:function(){return this._matrix[0]},set:function(g){this._matrix[0]=g}}),Object.defineProperty(vt.prototype,"b",{get:function(){return this._matrix[1]},set:function(g){this._matrix[1]=g}}),Object.defineProperty(vt.prototype,"c",{get:function(){return this._matrix[2]},set:function(g){this._matrix[2]=g}}),Object.defineProperty(vt.prototype,"d",{get:function(){return this._matrix[3]},set:function(g){this._matrix[3]=g}}),Object.defineProperty(vt.prototype,"e",{get:function(){return this._matrix[4]},set:function(g){this._matrix[4]=g}}),Object.defineProperty(vt.prototype,"f",{get:function(){return this._matrix[5]},set:function(g){this._matrix[5]=g}}),Object.defineProperty(vt.prototype,"rotation",{get:function(){return Math.atan2(this.shx,this.sx)}}),Object.defineProperty(vt.prototype,"scaleX",{get:function(){return this.decompose().scale.sx}}),Object.defineProperty(vt.prototype,"scaleY",{get:function(){return this.decompose().scale.sy}}),Object.defineProperty(vt.prototype,"isIdentity",{get:function(){return 1===this.sx&&0===this.shy&&0===this.shx&&1===this.sy&&0===this.tx&&0===this.ty}}),vt.prototype.join=function(g){return[this.sx,this.shy,this.shx,this.sy,this.tx,this.ty].map(ue).join(g)},vt.prototype.multiply=function(g){return new vt(g.sx*this.sx+g.shy*this.shx,g.sx*this.shy+g.shy*this.sy,g.shx*this.sx+g.sy*this.shx,g.shx*this.shy+g.sy*this.sy,g.tx*this.sx+g.ty*this.shx+this.tx,g.tx*this.shy+g.ty*this.sy+this.ty)},vt.prototype.decompose=function(){var g=this.sx,k=this.shy,R=this.shx,V=this.sy,le=this.tx,ye=this.ty,De=Math.sqrt(g*g+k*k),it=(g/=De)*R+(k/=De)*V;R-=g*it,V-=k*it;var ft=Math.sqrt(R*R+V*V);return it/=ft,g*(V/=ft)>16&255,V=ft>>8&255,le=255&ft}if(void 0===V||void 0===ye&&R===V&&V===le)k="string"==typeof R?R+" "+De[0]:2===g.precision?Fe(R/255)+" "+De[0]:D(R/255)+" "+De[0];else if(void 0===ye||"object"===(0,en.Z)(ye)){if(ye&&!isNaN(ye.a)&&0===ye.a)return["1.","1.","1.",De[1]].join(" ");k="string"==typeof R?[R,V,le,De[1]].join(" "):2===g.precision?[Fe(R/255),Fe(V/255),Fe(le/255),De[1]].join(" "):[D(R/255),D(V/255),D(le/255),De[1]].join(" ")}else k="string"==typeof R?[R,V,le,ye,De[2]].join(" "):2===g.precision?[Fe(R),Fe(V),Fe(le),Fe(ye),De[2]].join(" "):[D(R),D(V),D(le),D(ye),De[2]].join(" ");return k},Oi=v.__private__.getFilters=function(){return o},Zr=v.__private__.putStream=function(g){var k=(g=g||{}).data||"",R=g.filters||Oi(),V=g.alreadyAppliedFilters||[],le=g.addLength1||!1,ye=k.length,De=g.objectId,it=function(PA){return PA};if(null!==B&&void 0===De)throw new Error("ObjectId must be passed to putStream for file encryption");null!==B&&(it=OA.encryptor(De,0));var ft={};!0===R&&(R=["FlateEncode"]);var Mt=g.additionalKeyValues||[],Ht=(ft=void 0!==gt.API.processDataByFilters?gt.API.processDataByFilters(k,R):{data:k,reverseChain:[]}).reverseChain+(Array.isArray(V)?V.join(" "):V.toString());if(0!==ft.data.length&&(Mt.push({key:"Length",value:ft.data.length}),!0===le&&Mt.push({key:"Length1",value:ye})),0!=Ht.length)if(Ht.split("/").length-1==1)Mt.push({key:"Filter",value:Ht});else{Mt.push({key:"Filter",value:"["+Ht+"]"});for(var $t=0;$t>"),0!==ft.data.length&&(J("stream"),J(it(ft.data)),J("endstream"))},Pi=v.__private__.putPage=function(g){var k=g.number,R=g.data,V=g.objId,le=g.contentsObjId;vr(V,!0),J("<>"),J("endobj");var ye=R.join("\n");return"advanced"===X&&(ye+="\nQ"),vr(le,!0),Zr({data:ye,filters:Oi(),objectId:le}),J("endobj"),V},Vo=v.__private__.putPages=function(){var g,k,R=[];for(g=1;g<=Kn;g++)Ft[g].objId=sA(),Ft[g].contentsObjId=sA();for(g=1;g<=Kn;g++)R.push(Pi({number:g,data:je[g],objId:Ft[g].objId,contentsObjId:Ft[g].contentsObjId,mediaBox:Ft[g].mediaBox,cropBox:Ft[g].cropBox,bleedBox:Ft[g].bleedBox,trimBox:Ft[g].trimBox,artBox:Ft[g].artBox,userUnit:Ft[g].userUnit,rootDictionaryObjId:ss,resourceDictionaryObjId:ci}));vr(ss,!0),J("<>"),J("endobj"),gn.publish("postPutPages")},oc=function(g){gn.publish("putFont",{font:g,out:J,newObject:fA,putStream:Zr}),!0!==g.isAlreadyPutted&&(g.objectNumber=fA(),J("<<"),J("/Type /Font"),J("/BaseFont /"+Xr(g.postScriptName)),J("/Subtype /Type1"),"string"==typeof g.encoding&&J("/Encoding /"+g.encoding),J("/FirstChar 32"),J("/LastChar 255"),J(">>"),J("endobj"))},cc=function(g){g.objectNumber=fA();var k=[];k.push({key:"Type",value:"/XObject"}),k.push({key:"Subtype",value:"/Form"}),k.push({key:"BBox",value:"["+[ue(g.x),ue(g.y),ue(g.x+g.width),ue(g.y+g.height)].join(" ")+"]"}),k.push({key:"Matrix",value:"["+g.matrix.toString()+"]"});var R=g.pages[1].join("\n");Zr({data:R,additionalKeyValues:k,objectId:g.objectNumber}),J("endobj")},Ig=function(g,k){k||(k=21);var R=fA(),V=function(g,k){var R,V=[],le=1/(k-1);for(R=0;R<1;R+=le)V.push(R);V.push(1),0!=g[0].offset&&g.unshift({offset:0,color:g[0].color}),1!=g[g.length-1].offset&&g.push({offset:1,color:g[g.length-1].color});for(var it="",ft=0,Mt=0;Mtg[ft+1].offset;)ft++;var Ht=g[ft].offset,$t=(R-Ht)/(g[ft+1].offset-Ht),Hn=g[ft].color,Jn=g[ft+1].color;it+=ne(Math.round((1-$t)*Hn[0]+$t*Jn[0]).toString(16))+ne(Math.round((1-$t)*Hn[1]+$t*Jn[1]).toString(16))+ne(Math.round((1-$t)*Hn[2]+$t*Jn[2]).toString(16))}return it.trim()}(g.colors,k),le=[];le.push({key:"FunctionType",value:"0"}),le.push({key:"Domain",value:"[0.0 1.0]"}),le.push({key:"Size",value:"["+k+"]"}),le.push({key:"BitsPerSample",value:"8"}),le.push({key:"Range",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),le.push({key:"Decode",value:"[0.0 1.0 0.0 1.0 0.0 1.0]"}),Zr({data:V,additionalKeyValues:le,alreadyAppliedFilters:["/ASCIIHexDecode"],objectId:R}),J("endobj"),g.objectNumber=fA(),J("<< /ShadingType "+g.type),J("/ColorSpace /DeviceRGB");var ye="/Coords ["+ue(parseFloat(g.coords[0]))+" "+ue(parseFloat(g.coords[1]))+" ";ye+=2===g.type?ue(parseFloat(g.coords[2]))+" "+ue(parseFloat(g.coords[3])):ue(parseFloat(g.coords[2]))+" "+ue(parseFloat(g.coords[3]))+" "+ue(parseFloat(g.coords[4]))+" "+ue(parseFloat(g.coords[5])),J(ye+="]"),g.matrix&&J("/Matrix ["+g.matrix.toString()+"]"),J("/Function "+R+" 0 R"),J("/Extend [true true]"),J(">>"),J("endobj")},Mg=function(g,k){var R=sA(),V=fA();k.push({resourcesOid:R,objectOid:V}),g.objectNumber=V;var le=[];le.push({key:"Type",value:"/Pattern"}),le.push({key:"PatternType",value:"1"}),le.push({key:"PaintType",value:"1"}),le.push({key:"TilingType",value:"1"}),le.push({key:"BBox",value:"["+g.boundingBox.map(ue).join(" ")+"]"}),le.push({key:"XStep",value:ue(g.xStep)}),le.push({key:"YStep",value:ue(g.yStep)}),le.push({key:"Resources",value:R+" 0 R"}),g.matrix&&le.push({key:"Matrix",value:"["+g.matrix.toString()+"]"}),Zr({data:g.stream,additionalKeyValues:le,objectId:g.objectNumber}),J("endobj")},hh=function(g){for(var k in g.objectNumber=fA(),J("<<"),g)switch(k){case"opacity":J("/ca "+Fe(g[k]));break;case"stroke-opacity":J("/CA "+Fe(g[k]))}J(">>"),J("endobj")},eA=function(g){vr(g.resourcesOid,!0),J("<<"),J("/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]"),function(){for(var g in J("/Font <<"),bn)bn.hasOwnProperty(g)&&(!1===x||!0===x&&y.hasOwnProperty(g))&&J("/"+g+" "+bn[g].objectNumber+" 0 R");J(">>")}(),function(){if(Object.keys(fn).length>0){for(var g in J("/Shading <<"),fn)fn.hasOwnProperty(g)&&fn[g]instanceof xi&&fn[g].objectNumber>=0&&J("/"+g+" "+fn[g].objectNumber+" 0 R");gn.publish("putShadingPatternDict"),J(">>")}}(),function(g){if(Object.keys(fn).length>0){for(var k in J("/Pattern <<"),fn)fn.hasOwnProperty(k)&&fn[k]instanceof v.TilingPattern&&fn[k].objectNumber>=0&&fn[k].objectNumber>")}}(g.objectOid),function(){if(Object.keys(Gn).length>0){var g;for(g in J("/ExtGState <<"),Gn)Gn.hasOwnProperty(g)&&Gn[g].objectNumber>=0&&J("/"+g+" "+Gn[g].objectNumber+" 0 R");gn.publish("putGStateDict"),J(">>")}}(),function(){for(var g in J("/XObject <<"),TA)TA.hasOwnProperty(g)&&TA[g].objectNumber>=0&&J("/"+g+" "+TA[g].objectNumber+" 0 R");gn.publish("putXobjectDict"),J(">>")}(),J(">>"),J("endobj")},mh=function(g){In[g.fontName]=In[g.fontName]||{},In[g.fontName][g.fontStyle]=g.id},Kl=function(g,k,R,V,le){var ye={id:"F"+(Object.keys(bn).length+1).toString(10),postScriptName:g,fontName:k,fontStyle:R,encoding:V,isStandardFont:le||!1,metadata:{}};return gn.publish("addFont",{font:ye,instance:this}),bn[ye.id]=ye,mh(ye),ye.id},lr=v.__private__.pdfEscape=v.pdfEscape=function(g,k){return function(g,k){var R,V,le,ye,De,it,ft,Mt,Ht;if(le=(k=k||{}).sourceEncoding||"Unicode",De=k.outputEncoding,(k.autoencode||De)&&bn[Gt].metadata&&bn[Gt].metadata[le]&&bn[Gt].metadata[le].encoding&&(ye=bn[Gt].metadata[le].encoding,!De&&bn[Gt].encoding&&(De=bn[Gt].encoding),!De&&ye.codePages&&(De=ye.codePages[0]),"string"==typeof De&&(De=ye[De]),De)){for(ft=!1,it=[],R=0,V=g.length;R>8&&(ft=!0);g=it.join("")}for(R=g.length;void 0===ft&&0!==R;)g.charCodeAt(R-1)>>8&&(ft=!0),R--;if(!ft)return g;for(it=k.noBOM?[]:[254,255],R=0,V=g.length;R>8)>>8)throw new Error("Character at position "+R+" of string '"+g+"' exceeds 16bits. Cannot be encoded into UCS-2 BE");it.push(Ht),it.push(Mt-(Ht<<8))}return String.fromCharCode.apply(void 0,it)}(g,k).replace(/\\/g,"\\\\").replace(/\(/g,"\\(").replace(/\)/g,"\\)")},Jl=v.__private__.beginPage=function(g){je[++Kn]=[],Ft[Kn]={objId:0,contentsObjId:0,userUnit:Number(h),artBox:null,bleedBox:null,cropBox:null,trimBox:null,mediaBox:{bottomLeftX:0,bottomLeftY:0,topRightX:Number(g[0]),topRightY:Number(g[1])}},wh(Kn),Te(je[G])},_h=function(g,k){var R,V,le;switch(A=k||A,"string"==typeof g&&(R=I(g.toLowerCase()),Array.isArray(R)&&(V=R[0],le=R[1])),Array.isArray(g)&&(V=g[0]*At,le=g[1]*At),isNaN(V)&&(V=n[0],le=n[1]),(V>14400||le>14400)&&(sn.warn("A page in a PDF can not be wider or taller than 14400 userUnit. jsPDF limits the width/height to 14400"),V=Math.min(14400,V),le=Math.min(14400,le)),n=[V,le],A.substr(0,1)){case"l":le>V&&(n=[le,V]);break;case"p":V>le&&(n=[le,V])}Jl(n),yh(Wl),J(hi),0!==$l&&J($l+" J"),0!==ed&&J(ed+" j"),gn.publish("addPage",{pageNumber:Kn})},Dg=function(g){g>0&&g<=Kn&&(je.splice(g,1),Ft.splice(g,1),Kn--,G>Kn&&(G=Kn),this.setPage(G))},wh=function(g){g>0&&g<=Kn&&(G=g)},Hg=v.__private__.getNumberOfPages=v.getNumberOfPages=function(){return je.length-1},vh=function(g,k,R){var V,le=void 0;return R=R||{},k=void 0!==k?k:bn[Gt].fontStyle,V=(g=void 0!==g?g:bn[Gt].fontName).toLowerCase(),void 0!==In[V]&&void 0!==In[V][k]?le=In[V][k]:void 0!==In[g]&&void 0!==In[g][k]?le=In[g][k]:!1===R.disableWarning&&sn.warn("Unable to look up font label for font '"+g+"', '"+k+"'. Refer to getFontList() for available fonts."),le||R.noFallback||null==(le=In.times[k])&&(le=In.times.normal),le},Zg=v.__private__.putInfo=function(){var g=fA(),k=function(V){return V};for(var R in null!==B&&(k=OA.encryptor(g,0)),J("<<"),J("/Producer ("+lr(k("jsPDF "+gt.version))+")"),Tt)Tt.hasOwnProperty(R)&&Tt[R]&&J("/"+R.substr(0,1).toUpperCase()+R.substr(1)+" ("+lr(k(Tt[R]))+")");J("/CreationDate ("+lr(k(be))+")"),J(">>"),J("endobj")},Yl=v.__private__.putCatalog=function(g){var k=(g=g||{}).rootDictionaryObjId||ss;switch(fA(),J("<<"),J("/Type /Catalog"),J("/Pages "+k+" 0 R"),nt||(nt="fullwidth"),nt){case"fullwidth":J("/OpenAction [3 0 R /FitH null]");break;case"fullheight":J("/OpenAction [3 0 R /FitV null]");break;case"fullpage":J("/OpenAction [3 0 R /Fit]");break;case"original":J("/OpenAction [3 0 R /XYZ null null 1]");break;default:var R=""+nt;"%"===R.substr(R.length-1)&&(nt=parseInt(nt)/100),"number"==typeof nt&&J("/OpenAction [3 0 R /XYZ null null "+Fe(nt)+"]")}switch(Rt||(Rt="continuous"),Rt){case"continuous":J("/PageLayout /OneColumn");break;case"single":J("/PageLayout /SinglePage");break;case"two":case"twoleft":J("/PageLayout /TwoColumnLeft");break;case"tworight":J("/PageLayout /TwoColumnRight")}Qt&&J("/PageMode /"+Qt),gn.publish("putCatalog"),J(">>"),J("endobj")},jg=v.__private__.putTrailer=function(){J("trailer"),J("<<"),J("/Size "+(ae+1)),J("/Root "+ae+" 0 R"),J("/Info "+(ae-1)+" 0 R"),null!==B&&J("/Encrypt "+OA.oid+" 0 R"),J("/ID [ <"+ve+"> <"+ve+"> ]"),J(">>")},Rg=v.__private__.putHeader=function(){J("%PDF-"+M),J("%\xba\xdf\xac\xe0")},Gg=v.__private__.putXRef=function(){var g="0000000000";J("xref"),J("0 "+(ae+1)),J("0000000000 65535 f ");for(var k=1;k<=ae;k++)J("function"==typeof ge[k]?(g+ge[k]()).slice(-10)+" 00000 n ":void 0!==ge[k]?(g+ge[k]).slice(-10)+" 00000 n ":"0000000000 00000 n ")},os=v.__private__.buildDocument=function(){ae=0,Ce=0,we=[],ge=[],Re=[],ss=sA(),ci=sA(),Te(we),gn.publish("buildDocument"),Rg(),Vo(),function(){gn.publish("putAdditionalObjects");for(var g=0;g"),J("/O <"+OA.toHexString(OA.O)+">"),J("/P "+OA.P),J(">>"),J("endobj")),Zg(),Yl();var g=Ce;return Gg(),jg(),J("startxref"),J(""+g),J("%%EOF"),Te(je[G]),we.join("\n")},pc=v.__private__.getBlob=function(g){return new Blob([ut(g)],{type:"application/pdf"})},fc=v.output=v.__private__.output=((g=function(g,k){switch("string"==typeof(k=k||{})?k={filename:k}:k.filename=k.filename||"generated.pdf",g){case void 0:return os();case"save":v.save(k.filename);break;case"arraybuffer":return ut(os());case"blob":return pc(os());case"bloburi":case"bloburl":if(void 0!==mt.URL&&"function"==typeof mt.URL.createObjectURL)return mt.URL&&mt.URL.createObjectURL(pc(os()))||void 0;sn.warn("bloburl is not supported by your system, because URL.createObjectURL is not supported by your browser.");break;case"datauristring":case"dataurlstring":var R="",V=os();try{R=Qr(V)}catch{R=Qr(unescape(encodeURIComponent(V)))}return"data:application/pdf;filename="+k.filename+";base64,"+R;case"pdfobjectnewwindow":if("[object Window]"===Object.prototype.toString.call(mt)){var le="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",ye=' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';k.pdfObjectUrl&&(le=k.pdfObjectUrl,ye="");var De='