From 35826bddf96c13735484323b6b1a53491ba68458 Mon Sep 17 00:00:00 2001 From: tim-s-ccs Date: Mon, 22 Aug 2022 09:49:32 +0100 Subject: [PATCH] I have decided to convert all the JavaScript in the application to TypeScript. We have quite a lot of JavaScript in this application and I do find it quite difficult to maintain. I am hopeful TypeScript will make all the maintenance easier and help future proof our code. It also gave me an opportunity to review our code and remove anything that was no longer needed. (I also had nothing else to do at this moment in time and wanted to give myself a challenge) --- .gitignore | 3 + Dockerfile | 2 +- README.md | 105 +-- app/assets/javascripts/application.js | 7 +- app/assets/javascripts/common/common.js | 31 - .../javascripts/common/step-by-step-nav.js | 508 ---------- app/assets/javascripts/cookie-banner.js | 68 -- app/assets/javascripts/cp/cp-sign-in.js | 45 - .../buildings/add-address.js | 41 - .../buildings/building-type.js | 28 - .../choose-locations-assistant.js | 24 - .../choose-services-assistant.js | 27 - .../chooser-component.js | 311 ------ .../details/contract-extensions.js | 272 ------ .../fm-contact-detail.js | 80 -- .../facilities-management/fm-find-address.js | 402 -------- .../fm-number-sections.js | 47 - .../fm-procurement-building.js | 49 - .../facilities-management/fm-table-filter.js | 24 - .../fm-upload-progress.js | 66 -- .../admin/fm-management-report-progress.js | 49 - .../procurement/choose-services-buildings.js | 37 - .../rm3830/procurement/contract-details.js | 21 - .../procurement/form-validation-component.js | 556 ----------- .../procurement-buildings-services-lifts.js | 70 -- .../procurement/procurement-pension-funds.js | 64 -- .../procurement/quick-search-results.js | 23 - .../rm6232/admin/supplier-data-snapshot.js | 17 - app/assets/javascripts/global/global.js | 171 ---- app/assets/javascripts/google-analytics.js | 33 - .../admin/management_reports_controller.rb | 2 +- app/helpers/application_helper.rb | 20 +- .../facilities_management/buildings_helper.rb | 4 + .../find_address_helper.rb | 49 +- .../procurement_buildings_services_helper.rb | 9 - .../procurements/contract_details_helper.rb | 9 - app/helpers/gov_uk_helper.rb | 1 + app/helpers/gov_uk_helper/select.rb | 30 + app/helpers/gov_uk_helper/text_input.rb | 2 +- app/typescript/.eslintrc.json | 38 + .../buildings/buildingType.ts | 26 + .../buildings/detailsInStorage.ts | 45 + .../facilitiesManagement/detailsInStorage.ts | 83 ++ .../facilitiesManagement/detailsLinks.ts | 25 + .../facilitiesManagement/filterTable.ts | 46 + .../facilitiesManagement/findAddress.ts | 668 +++++++++++++ .../facilitiesManagement/integerInput.ts | 9 + .../facilitiesManagement/numberWithCommas.ts | 30 + .../procurements/chooseServicesForBuilding.ts | 33 + .../procurements/contractExtensions.ts | 360 +++++++ .../procurements/resultsToggle.ts | 26 + .../procurements/selectRegion.ts | 49 + .../rm3830/addNestedAttributes.ts | 215 +++++ .../rm3830/admin/adminUploadProgress.ts} | 14 +- .../rm3830/admin/managementReport.ts | 14 + .../rm3830/bulkUploadProgress.ts} | 12 +- .../rm6232/admin/supplierDataSnapshot.ts | 23 + .../facilitiesManagement/uploadProgress.ts | 148 +++ app/typescript/shared/checkboxAccordion.ts | 246 +++++ app/typescript/shared/cookieBanner.ts | 80 ++ app/typescript/shared/googleAnalytics.ts | 39 + app/typescript/shared/govukFrontend.ts | 9 + app/typescript/shared/passwordStrength.ts | 54 ++ app/typescript/shared/stepByStepNav.ts | 229 +++++ .../edit_partials/_building_details.html.erb | 22 +- .../_postcode_lookup_container.html.erb | 85 -- .../_region_lookup_container.html.erb | 49 - ..._contact_detail_address_container.html.erb | 72 -- .../buyer_details/edit.html.erb | 98 +- .../_annual_contract_value.html.erb | 8 +- .../edit_partials/_contract_name.html.erb | 10 +- .../_mobilisation_period.html.erb | 8 +- .../_lifts.html.erb | 2 +- .../_new_contact_details.html.erb | 12 +- .../edit_partials/_pension_funds.html.erb | 2 +- .../_procurement_pension_fund.html.erb | 5 +- .../find_address/_find_address.html.erb | 144 +++ .../shared/find_address/_find_region.html.erb | 59 ++ app/views/layouts/_init-js.html.erb | 5 - app/views/layouts/application.html.erb | 1 - app/views/layouts/error.html.erb | 1 - .../views/facilities_management.en.yml | 57 +- .../views/facilities_management.rm3830.en.yml | 3 + features/helpers/responsive_headers_helper.rb | 4 + features/step_definitions/hooks.rb | 4 + features/support/pages/building.rb | 4 +- features/support/pages/buyer_detail.rb | 2 +- .../pages/rm3830/service_requirement.rb | 2 +- package.json | 11 +- .../management_reports_controller_spec.rb | 4 +- .../management_reports_controller_spec.rb | 4 +- tsconfig.json | 9 + yarn.lock | 887 ++++++++++++++++++ 93 files changed, 3879 insertions(+), 3523 deletions(-) delete mode 100644 app/assets/javascripts/common/common.js delete mode 100644 app/assets/javascripts/common/step-by-step-nav.js delete mode 100644 app/assets/javascripts/cookie-banner.js delete mode 100644 app/assets/javascripts/cp/cp-sign-in.js delete mode 100644 app/assets/javascripts/facilities-management/buildings/add-address.js delete mode 100644 app/assets/javascripts/facilities-management/buildings/building-type.js delete mode 100644 app/assets/javascripts/facilities-management/choose-locations-assistant.js delete mode 100644 app/assets/javascripts/facilities-management/choose-services-assistant.js delete mode 100644 app/assets/javascripts/facilities-management/chooser-component.js delete mode 100644 app/assets/javascripts/facilities-management/details/contract-extensions.js delete mode 100644 app/assets/javascripts/facilities-management/fm-contact-detail.js delete mode 100644 app/assets/javascripts/facilities-management/fm-find-address.js delete mode 100644 app/assets/javascripts/facilities-management/fm-number-sections.js delete mode 100644 app/assets/javascripts/facilities-management/fm-procurement-building.js delete mode 100644 app/assets/javascripts/facilities-management/fm-table-filter.js delete mode 100644 app/assets/javascripts/facilities-management/fm-upload-progress.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/admin/fm-management-report-progress.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/choose-services-buildings.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/contract-details.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/form-validation-component.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/procurement-buildings-services-lifts.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/procurement-pension-funds.js delete mode 100644 app/assets/javascripts/facilities-management/rm3830/procurement/quick-search-results.js delete mode 100644 app/assets/javascripts/facilities-management/rm6232/admin/supplier-data-snapshot.js delete mode 100644 app/assets/javascripts/global/global.js delete mode 100644 app/assets/javascripts/google-analytics.js create mode 100644 app/helpers/gov_uk_helper/select.rb create mode 100644 app/typescript/.eslintrc.json create mode 100644 app/typescript/facilitiesManagement/buildings/buildingType.ts create mode 100644 app/typescript/facilitiesManagement/buildings/detailsInStorage.ts create mode 100644 app/typescript/facilitiesManagement/detailsInStorage.ts create mode 100644 app/typescript/facilitiesManagement/detailsLinks.ts create mode 100644 app/typescript/facilitiesManagement/filterTable.ts create mode 100644 app/typescript/facilitiesManagement/findAddress.ts create mode 100644 app/typescript/facilitiesManagement/integerInput.ts create mode 100644 app/typescript/facilitiesManagement/numberWithCommas.ts create mode 100644 app/typescript/facilitiesManagement/procurements/chooseServicesForBuilding.ts create mode 100644 app/typescript/facilitiesManagement/procurements/contractExtensions.ts create mode 100644 app/typescript/facilitiesManagement/procurements/resultsToggle.ts create mode 100644 app/typescript/facilitiesManagement/procurements/selectRegion.ts create mode 100644 app/typescript/facilitiesManagement/rm3830/addNestedAttributes.ts rename app/{assets/javascripts/facilities-management/rm3830/admin/fm-admin-upload-prgress.js => typescript/facilitiesManagement/rm3830/admin/adminUploadProgress.ts} (75%) create mode 100644 app/typescript/facilitiesManagement/rm3830/admin/managementReport.ts rename app/{assets/javascripts/facilities-management/rm3830/fm-bulk-upload-progress.js => typescript/facilitiesManagement/rm3830/bulkUploadProgress.ts} (77%) create mode 100644 app/typescript/facilitiesManagement/rm6232/admin/supplierDataSnapshot.ts create mode 100644 app/typescript/facilitiesManagement/uploadProgress.ts create mode 100644 app/typescript/shared/checkboxAccordion.ts create mode 100644 app/typescript/shared/cookieBanner.ts create mode 100644 app/typescript/shared/googleAnalytics.ts create mode 100644 app/typescript/shared/govukFrontend.ts create mode 100644 app/typescript/shared/passwordStrength.ts create mode 100644 app/typescript/shared/stepByStepNav.ts delete mode 100644 app/views/facilities_management/buildings/edit_partials/_postcode_lookup_container.html.erb delete mode 100644 app/views/facilities_management/buildings/edit_partials/_region_lookup_container.html.erb delete mode 100644 app/views/facilities_management/buyer_details/_contact_detail_address_container.html.erb create mode 100644 app/views/facilities_management/shared/find_address/_find_address.html.erb create mode 100644 app/views/facilities_management/shared/find_address/_find_region.html.erb delete mode 100644 app/views/layouts/_init-js.html.erb create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 75631878d6..940e30ba62 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ launch.json # We are using yarn not npm, but this file sometimes gets added by snyk which brakes the pipeline package-lock.json + +# We will compile the typescript as part of the assets pipeline +app/assets/javascripts/dist/* diff --git a/Dockerfile b/Dockerfile index efd7a4ddaf..8815db6adf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -105,7 +105,7 @@ RUN bundle install # Install Node.js dependencies COPY package.json yarn.lock ./ -RUN yarn install --frozen-lockfile --production --no-cache +RUN yarn install --frozen-lockfile --production --no-cache --ignore-scripts COPY . . diff --git a/README.md b/README.md index 74143902e2..9e017d6068 100644 --- a/README.md +++ b/README.md @@ -37,9 +37,9 @@ make ### MacOS #### Check the Ruby version -##### N.B. The project currently runs on 2.7.4. (August 2021) +##### N.B. The project currently runs on 2.7.6 (April 2022) -Ensure that a ruby version manager (e.g. rvm or rbenv) is installed and set up properly, using 2.7.4 as the Ruby version before trying anything else. +Ensure that a ruby version manager (e.g. rvm or rbenv) is installed and set up properly, using 2.7.6 as the Ruby version before trying anything else. #### Install Postgres and PostGIS `brew install postgres` (this will install the latest (HEAD) version, currently 12. The server runs 11!) @@ -118,43 +118,11 @@ obtained from the [AWS Systems Manager Parameter Store][aws-parameter-store]. * If present, enable the Cognito sign-in link on the Supply Teachers gateway page -#### DfE Sign-in - -* `DFE_SIGNIN_URL` - * Obtained from DfE Sign-in. Should have just `/` as a path component, e.g. - `https://signin.example.com/`. -* `DFE_SIGNIN_CLIENT_ID` - * Obtained from DfE Sign-in. -* `DFE_SIGNIN_CLIENT_SECRET` - * Obtained from DfE Sign-in. -* `DFE_SIGNIN_REDIRECT_URI` - * A link to the authentication callback in this application, i.e. - `https://marketplace.service.crowncommercial.gov.uk/auth/dfe/callback` for - the live service -* `DFE_SIGNIN_WHITELISTED_EMAIL_ADDRESSES` - * Comma-separated list of email addresses allowed access via DfE Sign-in - * If this variable is not present, DfE Sign-in is not protected by - whitelisting - #### Google Analytics * `GA_TRACKING_ID` * Google Analytics is disabled if this is not set -#### Upload URL protection - -HTTP Basic Authentication credentials. Only needed in production environments. -See the [Uploading data section](#uploading-data) below. - -* `HTTP_BASIC_AUTH_NAME` -* `HTTP_BASIC_AUTH_PASSWORD` - -If the following environment variable is set then the app exposes routes for -uploading supplier data JSON. Otherwise those routes do not exist and users -receive a 404. - -* `APP_HAS_UPLOAD_PRIVILEGES` - #### Database The following are used to configure the database, but only in production @@ -181,47 +149,6 @@ rails s ``` Visit [localhost:3000](http://localhost:3000). - -## Uploading data - -You can upload data for a given framework using the following command where -`FRAMEWORK_NAME` is one of `supply-teachers`, `facilities-management` or -`management-consultancy`; `SCHEME` is one of `http` (local development) or -`https` (other environments); `HTTP_BASIC_AUTH_NAME` & -`HTTP_BASIC_AUTH_PASSWORD` credentials (only needed for production -environments): - -``` -git clone git@github.com:Crown-Commercial-Service/crown-marketplace-data.git -cd crown-marketplace-data/$FRAMEWORK_NAME -curl --user $HTTP_BASIC_AUTH_NAME:$HTTP_BASIC_AUTH_PASSWORD --request POST \ - --header "Content-Type: application/json" --data @output/data.json \ - $SCHEME://$HOST/$FRAMEWORK_NAME/uploads -``` - -### Audit trail - -The application keeps a record of each *successful* upload in the database. So, -for example, the time of the most recent upload for a framework can be obtained -using the Rails console with one of the following commands: - -* `FacilitiesManagement::Upload.order(:created_at).last.created_at` -* `ManagementConsultancy::Upload.order(:created_at).last.created_at` -* `SupplyTeachers::Upload.order(:created_at).last.created_at` - -## Regenerating error pages - -We use the [juice][] npm package to generate HTML error pages from the live -service, inlining all CSS, images, web fonts, etc. A Rake task makes this -easier: - -``` -rake 'error_pages[http://localhost:3000]' -``` - -This will pull down `/errors/404.html`, for example, and save an inlined copy in -`public/404.html`. - ## Development ### Design & frontend @@ -233,6 +160,7 @@ This will pull down `/errors/404.html`, for example, and save an inlined copy in [yarn][], and the exact versions of all dependencies direct/indirect are locked in `yarn.lock`. * The CSS follows [Block Element Modifier][] conventions. +* We use `TypeScript` to write our frontend code (in `app/typescript`) ### Code @@ -246,9 +174,6 @@ which are somewhat non-standard: framework into a separate Rails app if that was deemed appropriate down the line. -* The supplier-related data for each framework is bulk uploaded over HTTPS to be - stored in the database; the data is then effectively regarded as read-only. - * A bunch of other less-variable data is stored in CSV files in the code repository and made available to the application via classes including the `StaticRecord` concern, e.g. `Nuts3Region` which loads its data from @@ -308,9 +233,8 @@ classes. various types. * I18n translations are used in specs to reduce their sensitivity to copy changes. -* [RSpec feature specs][feature-specs] are used for acceptance testing. -* RSpec matchers from the [capybara][] gem are used throughout the specs to - make assertions about rendered HTML. +* We use the [Cucumber][] testing framework to test the frontend functionality with a subset of the test run as part of the CI. +* We use [Axe Cucumber][] to run accessibility tests but these are not run as part of the CI. * All the specs are run as part of the default Rake task, but the standard RSpec-provided Rake tasks also exist for running sub-groups of the specs. @@ -324,16 +248,13 @@ Note that some lines are excluded from simplecov with the `# :nocov:` instructio ### Continuous integration & deployment -* Continuous integration and deployment is implemented on the new AWS-based CCS - infrastructure. -* The tests are run automatically when a new commit is pushed to a triggering - branch of this repository on GitHub. This is achieved using - [AWS CodePipeline][] and [AWS CodeBuild][]. -* The `pipeline_pre_build_tests.sh` script is run to install everything that's - needed to run the tests on the fairly vanilla CodeBuild container. -* The `pipeline_run_build_tests.sh` script is what actually runs the tests. -* If all the tests pass, then a container is built using the `Dockerfile` in - this repo, uploaded to the [AWS Elastic Container Registry][], and deployed +* When a branch is pushed or pull request is raised GitHub actions will run + the rspec and cucumber test suites. +* When the PR is merged to a main branch GitHub actions will run the test suites + again before triggering the AWS pipeline. +* We use [AWS CodePipeline][] and [AWS CodeBuild][] to build and deploy the application. +* A container is built using the `Dockerfile` in this repo, + uploaded to the [AWS Elastic Container Registry][], and deployed using [AWS Elastic Container Service][]. * Environment variables for the various containers running on the AWS infrastructure are obtained from the @@ -365,3 +286,5 @@ Note that some lines are excluded from simplecov with the `# :nocov:` instructio [CMpDevEnvironment]: https://github.com/Crown-Commercial-Service/CMpDevEnvironment [CMp Developer Guide]: https://github.com/Crown-Commercial-Service/CMpDevEnvironment/blob/develop/docs/ccs_aws_v1-developer_guide.md [faker]: https://github.com/stympy/faker +[Cucumber]: https://cucumber.io/ +[Axe Cucumber]: https://www.deque.com/axe/ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 13971170d5..0de766ad43 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,9 +12,4 @@ //= require jquery3 //= require rails-ujs //= require govuk-frontend/govuk/all -//= require google-analytics -//= require cookie-banner -//= require_tree ./common -//= require_tree ./facilities-management -//= require cp/cp-sign-in.js -//= require global/global.js +//= require_tree ./dist diff --git a/app/assets/javascripts/common/common.js b/app/assets/javascripts/common/common.js deleted file mode 100644 index a76c08d0e3..0000000000 --- a/app/assets/javascripts/common/common.js +++ /dev/null @@ -1,31 +0,0 @@ -const common = { - - destructurePostCode(pc) { - const input = (`${pc}`).trim().toUpperCase(); - const regEx = /^(([A-Z][A-Z]{0,1})([0-9][A-Z0-9]{0,1})) {0,}(([0-9])([A-Z]{2}))$/i; - const matches = input.match(regEx); - - let result = { valid: false, input }; - if (matches !== null) { - result = { - valid: true, - fullPostcode: `${matches[1]} ${matches[4]}`, - postcodeArea: matches[2], - outCode: matches[1], - postcodeDistrict: matches[1], - inCode: matches[4], - postcodeSector: matches[5], - unitPostcode: matches[6], - formattedInput() { - return this.fullPostcode; - }, - }; - } - - return result; - }, - - getCachedData(key) { - if (localStorage) return JSON.parse(localStorage.getItem(key)) || []; - }, -}; diff --git a/app/assets/javascripts/common/step-by-step-nav.js b/app/assets/javascripts/common/step-by-step-nav.js deleted file mode 100644 index f83d3f7d67..0000000000 --- a/app/assets/javascripts/common/step-by-step-nav.js +++ /dev/null @@ -1,508 +0,0 @@ -window.GOVUKFrontend = window.GOVUKFrontend || {}; -window.GOVUKFrontend.Modules = window.GOVUKFrontend.Modules || {}; - -(function (Modules) { - function Gemstepnav($module) { - this.$module = $module; - this.$module.actions = {}; // stores text for JS appended elements 'show' and 'hide' on steps, and 'show/hide all' button - this.$module.rememberShownStep = false; - this.$module.stepNavSize = false; - this.$module.sessionStoreLink = 'govuk-step-nav-active-link'; - this.$module.activeLinkClass = 'gem-c-step-nav__list-item--active'; - this.$module.activeStepClass = 'gem-c-step-nav__step--active'; - this.$module.activeLinkHref = '#content'; - this.$module.uniqueId = false; - } - - Gemstepnav.prototype.init = function () { - // Indicate that js has worked - this.$module.classList.add('gem-c-step-nav--active'); - - // Prevent FOUC, remove class hiding content - this.$module.classList.remove('js-hidden'); - - this.$module.stepNavSize = this.$module.classList.contains('gem-c-step-nav--large') ? 'Big' : 'Small'; - this.$module.rememberShownStep = !!this.$module.hasAttribute('data-remember') && this.$module.stepNavSize === 'Big'; - - this.$module.steps = this.$module.querySelectorAll('.js-step'); - this.$module.stepHeaders = this.$module.querySelectorAll('.js-toggle-panel'); - this.$module.totalSteps = this.$module.querySelectorAll('.js-panel').length; - this.$module.totalLinks = this.$module.querySelectorAll('.gem-c-step-nav__link').length; - this.$module.showOrHideAllButton = false; - - this.$module.uniqueId = this.$module.getAttribute('data-id') || false; - - if (this.$module.uniqueId) { - this.$module.sessionStoreLink = `${this.$module.sessionStoreLink}_${this.$module.uniqueId}`; - } - - const stepNavTracker = new this.StepNavTracker(this.$module.uniqueId, this.$module.totalSteps, this.$module.totalLinks); - - this.getTextForInsertedElements(); - this.addButtonstoSteps(); - this.addShowHideAllButton(); - this.addShowHideToggle(); - this.addAriaControlsAttrForShowHideAllButton(); - - this.ensureOnlyOneActiveLink(); - this.showPreviouslyOpenedSteps(); - - this.bindToggleForSteps(stepNavTracker); - this.bindToggleShowHideAllButton(stepNavTracker); - this.bindComponentLinkClicks(stepNavTracker); - }; - - Gemstepnav.prototype.getTextForInsertedElements = function () { - this.$module.actions.showText = this.$module.getAttribute('data-show-text'); - this.$module.actions.hideText = this.$module.getAttribute('data-hide-text'); - this.$module.actions.showAllText = this.$module.getAttribute('data-show-all-text'); - this.$module.actions.hideAllText = this.$module.getAttribute('data-hide-all-text'); - }; - - Gemstepnav.prototype.addShowHideAllButton = function () { - const showAll = document.createElement('div'); - const steps = this.$module.querySelectorAll('.gem-c-step-nav__steps')[0]; - - showAll.className = 'gem-c-step-nav__controls govuk-!-display-none-print'; - showAll.innerHTML = `${''; - - this.$module.insertBefore(showAll, steps); - this.$module.showOrHideAllButton = this.$module.querySelectorAll('.js-step-controls-button')[0]; - }; - - Gemstepnav.prototype.addShowHideToggle = function () { - for (let i = 0; i < this.$module.stepHeaders.length; i++) { - const thisel = this.$module.stepHeaders[i]; - - if (!thisel.querySelectorAll('.js-toggle-link').length) { - const showHideSpan = document.createElement('span'); - const showHideSpanText = document.createElement('span'); - const showHideSpanIcon = document.createElement('span'); - const showHideSpanFocus = document.createElement('span'); - const thisSectionSpan = document.createElement('span'); - - showHideSpan.className = 'gem-c-step-nav__toggle-link js-toggle-link govuk-!-display-none-print'; - showHideSpanText.className = 'gem-c-step-nav__button-text js-toggle-link-text'; - showHideSpanIcon.className = 'gem-c-step-nav__chevron js-toggle-link-icon'; - showHideSpanFocus.className = 'gem-c-step-nav__toggle-link-focus'; - thisSectionSpan.className = 'govuk-visually-hidden'; - - showHideSpan.appendChild(showHideSpanFocus); - showHideSpanFocus.appendChild(showHideSpanIcon); - showHideSpanFocus.appendChild(showHideSpanText); - - thisSectionSpan.innerHTML = ' this section'; - showHideSpan.appendChild(thisSectionSpan); - - thisel.querySelectorAll('.js-step-title-button')[0].appendChild(showHideSpan); - } - } - }; - - Gemstepnav.prototype.headerIsOpen = function (stepHeader) { - return (typeof stepHeader.parentNode.getAttribute('show') !== 'undefined'); - }; - - Gemstepnav.prototype.addAriaControlsAttrForShowHideAllButton = function () { - const ariaControlsValue = this.$module.querySelectorAll('.js-panel')[0].getAttribute('id'); - - this.$module.showOrHideAllButton.setAttribute('aria-controls', ariaControlsValue); - }; - - // called by show all/hide all, sets all steps accordingly - Gemstepnav.prototype.setAllStepsShownState = function (isShown) { - const data = []; - - for (let i = 0; i < this.$module.steps.length; i++) { - const stepView = new this.StepView(this.$module.steps[i], this.$module); - stepView.setIsShown(isShown); - - if (isShown) { - data.push(this.$module.steps[i].getAttribute('id')); - } - } - - if (isShown) { - this.saveToSessionStorage(this.$module.uniqueId, JSON.stringify(data)); - } else { - this.removeFromSessionStorage(this.$module.uniqueId); - } - }; - - // called on load, determines whether each step should be open or closed - Gemstepnav.prototype.showPreviouslyOpenedSteps = function () { - const data = this.loadFromSessionStorage(this.$module.uniqueId) || []; - - for (let i = 0; i < this.$module.steps.length; i++) { - const thisel = this.$module.steps[i]; - const id = thisel.getAttribute('id'); - const stepView = new this.StepView(thisel, this.$module); - const shouldBeShown = thisel.hasAttribute('data-show'); - - // show the step if it has been remembered or if it has the 'data-show' attribute - if ((this.$module.rememberShownStep && data.indexOf(id) > -1) || (shouldBeShown && shouldBeShown !== 'undefined')) { - stepView.setIsShown(true); - } else { - stepView.setIsShown(false); - } - } - - if (data.length > 0) { - this.$module.showOrHideAllButton.setAttribute('aria-expanded', true); - this.setShowHideAllText(); - } - }; - - Gemstepnav.prototype.addButtonstoSteps = function () { - for (let i = 0; i < this.$module.steps.length; i++) { - const thisel = this.$module.steps[i]; - const title = thisel.querySelectorAll('.js-step-title')[0]; - const contentId = thisel.querySelectorAll('.js-panel')[0].getAttribute('id'); - const titleText = title.textContent || title.innerText; // IE8 fallback - - title.outerHTML = `${'' - + '' - + ''; - } - }; - - Gemstepnav.prototype.bindToggleForSteps = function (stepNavTracker) { - const that = this; - const togglePanels = this.$module.querySelectorAll('.js-toggle-panel'); - - for (let i = 0; i < togglePanels.length; i++) { - togglePanels[i].addEventListener('click', function (event) { - const stepView = new that.StepView(this.parentNode, that.$module); - stepView.toggle(); - - const stepIsOptional = this.parentNode.hasAttribute('data-optional'); - const toggleClick = new that.StepToggleClick(event, stepView, stepNavTracker, stepIsOptional, that.$module.stepNavSize); - toggleClick.trackClick(); - - that.setShowHideAllText(); - that.rememberStepState(this.parentNode); - }); - } - }; - - // if the step is open, store its id in session store - // if the step is closed, remove its id from session store - Gemstepnav.prototype.rememberStepState = function (step) { - if (this.$module.rememberShownStep) { - const data = JSON.parse(this.loadFromSessionStorage(this.$module.uniqueId)) || []; - const thisstep = step.getAttribute('id'); - const shown = step.classList.contains('step-is-shown'); - - if (shown) { - data.push(thisstep); - } else { - const i = data.indexOf(thisstep); - if (i > -1) { - data.splice(i, 1); - } - } - this.saveToSessionStorage(this.$module.uniqueId, JSON.stringify(data)); - } - }; - - // tracking click events on links in step content - Gemstepnav.prototype.bindComponentLinkClicks = function (stepNavTracker) { - const jsLinks = this.$module.querySelectorAll('.js-link'); - const that = this; - - for (let i = 0; i < jsLinks.length; i++) { - jsLinks[i].addEventListener('click', function (event) { - const dataPosition = this.getAttribute('data-position'); - const linkClick = new that.ComponentLinkClick(event, stepNavTracker, dataPosition, that.$module.stepNavSize); - linkClick.trackClick(); - - if (this.getAttribute('rel') !== 'external') { - that.saveToSessionStorage(that.$module.sessionStoreLink, dataPosition); - } - - if (this.getAttribute('href') === that.$module.activeLinkHref) { - that.setOnlyThisLinkActive(this); - that.setActiveStepClass(); - } - }); - } - }; - - Gemstepnav.prototype.saveToSessionStorage = function (key, value) { - window.sessionStorage.setItem(key, value); - }; - - Gemstepnav.prototype.loadFromSessionStorage = function (key, value) { - return window.sessionStorage.getItem(key); - }; - - Gemstepnav.prototype.removeFromSessionStorage = function (key) { - window.sessionStorage.removeItem(key); - }; - - Gemstepnav.prototype.setOnlyThisLinkActive = function (clicked) { - const allActiveLinks = this.$module.querySelectorAll(`.${this.$module.activeLinkClass}`); - for (let i = 0; i < allActiveLinks.length; i++) { - allActiveLinks[i].classList.remove(this.$module.activeLinkClass); - } - clicked.parentNode.classList.add(this.$module.activeLinkClass); - }; - - // if a link occurs more than once in a step nav, the backend doesn't know which one to highlight - // so it gives all those links the 'active' attribute and highlights the last step containing that link - // if the user clicked on one of those links previously, it will be in the session store - // this code ensures only that link and its corresponding step have the highlighting - // otherwise it accepts what the backend has already passed to the component - Gemstepnav.prototype.ensureOnlyOneActiveLink = function () { - const activeLinks = this.$module.querySelectorAll(`.js-list-item.${this.$module.activeLinkClass}`); - - if (activeLinks.length <= 1) { - return; - } - - const loaded = this.loadFromSessionStorage(this.$module.sessionStoreLink); - const activeParent = this.$module.querySelectorAll(`.${this.$module.activeLinkClass}`)[0]; - const activeChild = activeParent.firstChild; - const foundLink = activeChild.getAttribute('data-position'); - let lastClicked = loaded || foundLink; // the value saved has priority - - // it's possible for the saved link position value to not match any of the currently duplicate highlighted links - // so check this otherwise it'll take the highlighting off all of them - const checkLink = this.$module.querySelectorAll(`[data-position="${lastClicked}"]`)[0]; - - if (checkLink) { - if (!checkLink.parentNode.classList.contains(this.$module.activeLinkClass)) { - lastClicked = checkLink; - } - } else { - lastClicked = foundLink; - } - - this.removeActiveStateFromAllButCurrent(activeLinks, lastClicked); - this.setActiveStepClass(); - }; - - Gemstepnav.prototype.removeActiveStateFromAllButCurrent = function (activeLinks, current) { - for (let i = 0; i < activeLinks.length; i++) { - const thisel = activeLinks[i]; - if (thisel.querySelectorAll('.js-link')[0].getAttribute('data-position').toString() !== current.toString()) { - thisel.classList.remove(this.$module.activeLinkClass); - const visuallyHidden = thisel.querySelectorAll('.visuallyhidden'); - if (visuallyHidden.length) { - visuallyHidden[0].parentNode.removeChild(visuallyHidden[0]); - } - } - } - }; - - Gemstepnav.prototype.setActiveStepClass = function () { - // remove the 'active/open' state from all steps - const allActiveSteps = this.$module.querySelectorAll(`.${this.$module.activeStepClass}`); - for (let i = 0; i < allActiveSteps.length; i++) { - allActiveSteps[i].classList.remove(this.$module.activeStepClass); - allActiveSteps[i].removeAttribute('data-show'); - } - - // find the current page link and apply 'active/open' state to parent step - const activeLink = this.$module.querySelectorAll(`.${this.$module.activeLinkClass}`)[0]; - if (activeLink) { - const activeStep = activeLink.closest('.gem-c-step-nav__step'); - activeStep.classList.add(this.$module.activeStepClass); - activeStep.setAttribute('data-show', ''); - } - }; - - Gemstepnav.prototype.bindToggleShowHideAllButton = function (stepNavTracker) { - const that = this; - - this.$module.showOrHideAllButton.addEventListener('click', function (event) { - const textContent = this.textContent || this.innerText; - const shouldShowAll = textContent === that.$module.actions.showAllText; - - // Fire GA click tracking - stepNavTracker.trackClick('pageElementInteraction', (shouldShowAll ? 'stepNavAllShown' : 'stepNavAllHidden'), { - label: `${shouldShowAll ? that.$module.actions.showAllText : that.$module.actions.hideAllText}: ${that.$module.stepNavSize}`, - }); - - that.setAllStepsShownState(shouldShowAll); - that.$module.showOrHideAllButton.setAttribute('aria-expanded', shouldShowAll); - that.setShowHideAllText(); - - return false; - }); - }; - - Gemstepnav.prototype.setShowHideAllText = function () { - const shownSteps = this.$module.querySelectorAll('.step-is-shown').length; - const showAllChevon = this.$module.showOrHideAllButton.querySelector('.js-step-controls-button-icon'); - const showAllButtonText = this.$module.showOrHideAllButton.querySelector('.js-step-controls-button-text'); - // Find out if the number of is-opens == total number of steps - const shownStepsIsTotalSteps = shownSteps === this.$module.totalSteps; - - if (shownStepsIsTotalSteps) { - showAllButtonText.innerHTML = this.$module.actions.hideAllText; - showAllChevon.classList.remove('gem-c-step-nav__chevron--down'); - } else { - showAllButtonText.innerHTML = this.$module.actions.showAllText; - showAllChevon.classList.add('gem-c-step-nav__chevron--down'); - } - }; - - Gemstepnav.prototype.StepView = function (stepElement, $module) { - this.stepElement = stepElement; - this.stepContent = this.stepElement.querySelectorAll('.js-panel')[0]; - this.titleButton = this.stepElement.querySelectorAll('.js-step-title-button')[0]; - const textElement = this.stepElement.querySelectorAll('.js-step-title-text')[0]; - this.title = textElement.textContent || textElement.innerText; - this.title = this.title.replace(/^\s+|\s+$/g, ''); // this is 'trim' but supporting IE8 - this.showText = $module.actions.showText; - this.hideText = $module.actions.hideText; - this.upChevronSvg = $module.upChevronSvg; - this.downChevronSvg = $module.downChevronSvg; - - this.show = function () { - this.setIsShown(true); - }; - - this.hide = function () { - this.setIsShown(false); - }; - - this.toggle = function () { - this.setIsShown(this.isHidden()); - }; - - this.setIsShown = function (isShown) { - const toggleLink = this.stepElement.querySelectorAll('.js-toggle-link')[0]; - const toggleLinkText = toggleLink.querySelector('.js-toggle-link-text'); - const stepChevron = toggleLink.querySelector('.js-toggle-link-icon'); - - if (isShown) { - this.stepElement.classList.add('step-is-shown'); - this.stepContent.classList.remove('js-hidden'); - toggleLinkText.innerHTML = this.hideText; - stepChevron.classList.remove('gem-c-step-nav__chevron--down'); - } else { - this.stepElement.classList.remove('step-is-shown'); - this.stepContent.classList.add('js-hidden'); - toggleLinkText.innerHTML = this.showText; - stepChevron.classList.add('gem-c-step-nav__chevron--down'); - } - this.titleButton.setAttribute('aria-expanded', isShown); - }; - - this.isShown = function () { - return this.stepElement.classList.contains('step-is-shown'); - }; - - this.isHidden = function () { - return !this.isShown(); - }; - - this.numberOfContentItems = function () { - return this.stepContent.querySelectorAll('.js-link').length; - }; - }; - - Gemstepnav.prototype.StepToggleClick = function (event, stepView, stepNavTracker, stepIsOptional, stepNavSize) { - this.target = event.target; - this.stepIsOptional = stepIsOptional; - this.stepNavSize = stepNavSize; - - this.trackClick = function () { - const trackingOptions = { label: this.trackingLabel(), dimension28: stepView.numberOfContentItems().toString() }; - stepNavTracker.trackClick('pageElementInteraction', this.trackingAction(), trackingOptions); - }; - - this.trackingLabel = function () { - const clickedNearbyToggle = this.target.closest('.js-step').querySelectorAll('.js-toggle-panel')[0]; - return `${clickedNearbyToggle.getAttribute('data-position')} - ${stepView.title} - ${this.locateClickElement()}: ${this.stepNavSize}${this.isOptional()}`; - }; - - // returns index of the clicked step in the overall number of steps - this.stepIndex = function () { // eslint-disable-line no-unused-vars - return this.$module.steps.index(stepView.element) + 1; - }; - - this.trackingAction = function () { - return (stepView.isHidden() ? 'stepNavHidden' : 'stepNavShown'); - }; - - this.locateClickElement = function () { - if (this.clickedOnIcon()) { - return `${this.iconType()} click`; - } if (this.clickedOnHeading()) { - return 'Heading click'; - } - return 'Elsewhere click'; - }; - - this.clickedOnIcon = function () { - return this.target.classList.contains('js-toggle-link'); - }; - - this.clickedOnHeading = function () { - return this.target.classList.contains('js-step-title-text'); - }; - - this.iconType = function () { - return (stepView.isHidden() ? 'Minus' : 'Plus'); - }; - - this.isOptional = function () { - return (this.stepIsOptional ? ' ; optional' : ''); - }; - }; - - Gemstepnav.prototype.ComponentLinkClick = function (event, stepNavTracker, linkPosition, size) { - this.size = size; - this.target = event.target; - - this.trackClick = function () { - const trackingOptions = { label: `${this.target.getAttribute('href')} : ${this.size}` }; - const dimension28 = this.target.closest('.gem-c-step-nav__list').getAttribute('data-length'); - - if (dimension28) { - trackingOptions.dimension28 = dimension28; - } - - stepNavTracker.trackClick('stepNavLinkClicked', linkPosition, trackingOptions); - }; - }; - - // A helper that sends a custom event request to Google Analytics if - // the GOVUK module is setup - Gemstepnav.prototype.StepNavTracker = function (uniqueId, totalSteps, totalLinks) { - this.totalSteps = totalSteps; - this.totalLinks = totalLinks; - this.uniqueId = uniqueId; - - this.trackClick = function (category, action, options) { - // dimension26 records the total number of expand/collapse steps in this step nav - // dimension27 records the total number of links in this step nav - // dimension28 records the number of links in the step that was shown/hidden (handled in click event) - if (window.GOVUKFrontend.analytics && window.GOVUKFrontend.analytics.trackEvent) { - options = options || {}; - options.dimension26 = options.dimension26 || this.totalSteps.toString(); - options.dimension27 = options.dimension27 || this.totalLinks.toString(); - options.dimension96 = options.dimension96 || this.uniqueId; - window.GOVUKFrontend.analytics.trackEvent(category, action, options); - } - }; - }; - - Modules.Gemstepnav = Gemstepnav; -}(window.GOVUKFrontend.Modules)); diff --git a/app/assets/javascripts/cookie-banner.js b/app/assets/javascripts/cookie-banner.js deleted file mode 100644 index 64dfd0feed..0000000000 --- a/app/assets/javascripts/cookie-banner.js +++ /dev/null @@ -1,68 +0,0 @@ - -const removeGACookies = (formData, successFunction) => { - let success = false; - - $.ajax({ - type: 'PUT', - url: '/api/v2/update-cookie-settings', - data: formData, - dataType: 'json', - success() { - success = true; - }, - complete() { - if (success) successFunction(); - }, - }); -}; - -const cookiesSaved = () => { - $('#cookie-settings-saved').show(); - $('html, body').animate({ scrollTop: $('#cookie-settings-saved').offset().top }, 'slow'); -}; - -const cookieSettingsViewed = ($newBanner) => { - $('#cookie-options-container').hide(); - $newBanner.show(); -}; - -const updateBanner = (isAccepeted, $newBanner) => { - removeGACookies( - { - ga_cookie_usage: isAccepeted, - glassbox_cookie_usage: isAccepeted, - }, - cookieSettingsViewed.bind(null, $newBanner), - ); -}; - -$(() => { - const obsoleteCookies = ['crown_marketplace_cookie_settings_viewed', 'crown_marketplace_google_analytics_enabled']; - - obsoleteCookies.forEach((cookieName) => { - if (document.cookie.includes(`${cookieName}=`)) document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; - }); - - $('[name="cookies"]').on('click', (event) => { - event.preventDefault(); - - const buttonValue = event.currentTarget.value; - - updateBanner(buttonValue === 'accept', $(`#cookies-${buttonValue}ed-container`)); - }); - - const $form = $('#update-cookie-setings'); - - $form.on('submit', (event) => { - event.preventDefault(); - - $('#cookie-settings-saved').show(); - - const formData = Object.fromEntries($form.serializeArray().map((element) => [element.name, element.value])); - - removeGACookies( - formData, - cookiesSaved, - ); - }); -}); diff --git a/app/assets/javascripts/cp/cp-sign-in.js b/app/assets/javascripts/cp/cp-sign-in.js deleted file mode 100644 index ecc6444ad0..0000000000 --- a/app/assets/javascripts/cp/cp-sign-in.js +++ /dev/null @@ -1,45 +0,0 @@ -function cReg() { - return new RegExp('^.{8,}'); -} -function pReg() { - return new RegExp('^(?=.*?[#?!@£$%^&*-])'); -} -function uReg() { - return new RegExp('^(?=.*?[A-Z])'); -} -function numReg() { - return new RegExp('^(?=.*[0-9])'); -} - -function passwordStrength(input) { - const theTests = [ - [cReg(), $('#passeight')], - [pReg(), $('#passsymbol')], - [uReg(), $('#passcap')], - [numReg(), $('#passnum')], - ]; - - input.on('keyup', () => { - theTests.forEach((test) => { - if (test[0].test(input.val())) { - test[1].removeClass('wrong').addClass('correct'); - } else { - test[1].removeClass('correct').addClass('wrong'); - } - }); - }); -} - -$(() => { - const form = $('#main-content form.ccs-form'); - - if (form.length) { - const formIDs = ['cop_sign_in_form', 'cop_change_password_form', 'cop_register', 'cop_confirmation_code', 'cog_forgot_password_request_form', 'cog_forgot_password_reset_form']; - - formIDs.forEach((formID) => { - if (form.is(`#${formID}`)) { - passwordStrength($('#password01')); - } - }); - } -}); diff --git a/app/assets/javascripts/facilities-management/buildings/add-address.js b/app/assets/javascripts/facilities-management/buildings/add-address.js deleted file mode 100644 index dfb1248723..0000000000 --- a/app/assets/javascripts/facilities-management/buildings/add-address.js +++ /dev/null @@ -1,41 +0,0 @@ -$(() => { - const buildingName = $('#facilities_management_building_building_name'); - const buildingDescription = $('#facilities_management_building_description'); - const formBuildingDetails = $('#building-details-form'); - const formBuildingAddressDetails = $('#building-address-details-form'); - - function getDetails() { - window.sessionStorage.buildingDetailsBuildingName = buildingName.val(); - window.sessionStorage.buildingDetailsBuildingDescription = buildingDescription.val(); - window.sessionStorage.buildingDetails = true; - } - - function fillInDetails() { - buildingName.val(window.sessionStorage.buildingDetailsBuildingName); - buildingDescription.val(window.sessionStorage.buildingDetailsBuildingDescription); - } - - function removeDetails() { - window.sessionStorage.removeItem('buildingDetails'); - window.sessionStorage.removeItem('buildingDetailsBuildingName'); - window.sessionStorage.removeItem('buildingDetailsBuildingDescription'); - } - - if (typeof (Storage) !== 'undefined') { - if ($('.edit_facilities_management_building').length) { - if (formBuildingDetails.length && window.sessionStorage.buildingDetails) { - fillInDetails(formBuildingDetails); - } - - if (formBuildingDetails.length) { - $('#cant-find-address-link').on('click', () => { - getDetails(); - }); - } else if (!formBuildingAddressDetails) { - removeDetails(); - } - } else { - removeDetails(); - } - } -}); diff --git a/app/assets/javascripts/facilities-management/buildings/building-type.js b/app/assets/javascripts/facilities-management/buildings/building-type.js deleted file mode 100644 index 2ea38f0f53..0000000000 --- a/app/assets/javascripts/facilities-management/buildings/building-type.js +++ /dev/null @@ -1,28 +0,0 @@ -$(() => { - if ($('.building-type').length) { - const enableRadios = (detailsOpen) => { - const radioInputs = $('.govuk-details__text .govuk-radios__item'); - if (detailsOpen === true) { - radioInputs.each(function () { - $(this).find('input').removeAttr('disabled'); - }); - } else { - radioInputs.each(function () { - $(this).find('input').attr('disabled', 'disabled'); - }); - } - }; - - const govukDetails = document.querySelector('.govuk-details'); - - const observer = new MutationObserver(() => { - enableRadios(govukDetails.open); - }); - - const config = { attributes: true, childList: false, characterData: false }; - - observer.observe(govukDetails, config); - - enableRadios($('.govuk-details').attr('open') === 'open'); - } -}); diff --git a/app/assets/javascripts/facilities-management/choose-locations-assistant.js b/app/assets/javascripts/facilities-management/choose-locations-assistant.js deleted file mode 100644 index 6bf21253b8..0000000000 --- a/app/assets/javascripts/facilities-management/choose-locations-assistant.js +++ /dev/null @@ -1,24 +0,0 @@ -$(() => { - function initialiseChooseLocations() { - const obj = new ChooserComponent('procurement', 'locations', common.getCachedData('fm-locations')); - if (obj.validate()) { - return obj; - } - return null; - } - - if ($('.chooser-component').length > 0) { - try { - let activeChooser = null; - activeChooser = initialiseChooseLocations(); - - if (activeChooser !== null) { - activeChooser.init(); - activeChooser.PrimeBasket(); - } - } catch (e) { - console.log(e); - console.log('No location chooser component found'); - } - } -}); diff --git a/app/assets/javascripts/facilities-management/choose-services-assistant.js b/app/assets/javascripts/facilities-management/choose-services-assistant.js deleted file mode 100644 index ce2f59575b..0000000000 --- a/app/assets/javascripts/facilities-management/choose-services-assistant.js +++ /dev/null @@ -1,27 +0,0 @@ -$(() => { - function initialiseChooseServices() { - const obj = new ChooserComponent('procurement', 'services', common.getCachedData('fm-locations')); - if (obj.validate()) { - return obj; - } - return null; - } - - if ($('.chooser-component').length > 0) { - try { - let activeChooser = null; - if (activeChooser === null) { - if ($('.services').length > 0) { - activeChooser = initialiseChooseServices(); - } - } - - if (activeChooser !== null) { - activeChooser.init(); - activeChooser.PrimeBasket(); - } - } catch (e) { - console.log('No service chooser component found'); - } - } -}); diff --git a/app/assets/javascripts/facilities-management/chooser-component.js b/app/assets/javascripts/facilities-management/chooser-component.js deleted file mode 100644 index 8cfc1037c2..0000000000 --- a/app/assets/javascripts/facilities-management/chooser-component.js +++ /dev/null @@ -1,311 +0,0 @@ -function ChooserComponent(baseClass, classification, selectedItems) { - this._baseClass = baseClass; - this._classification = classification; - this._parentElementName = `div.${this._baseClass}`; - this._chooserElementName = `${this._parentElementName} .chooser-component.${this._classification}`; - this._checkboxSourceDivName = `${this._chooserElementName} div.chooser-input`; - this._basketName = `${this._parentElementName} .basket`; - this._basketContainer = null; - this._allCheckboxes = []; - this._sections = []; - this.selectedItems = selectedItems == null ? [] : selectedItems; -} - -ChooserComponent.prototype.validate = function () { - let result = false; - - result = ($(this._chooserElementName)).length > 0; - - return result; -}; -ChooserComponent.prototype.init = function () { - const result = false; - $('body').addClass('js-enabled'); - this._basketContainer = new BasketComponent(this._baseClass, this._classification, $(this._basketName), this.handleBasketRemove.bind(this)); - const sectionArray = $(`${this._checkboxSourceDivName} .chooser-section`); - for (let index = 0; index < sectionArray.length; index++) { - const elem = $(sectionArray[index]); - const id = elem.data('section'); - const name = elem.data('sectionname'); - if (id !== undefined && name !== undefined) { - const newSection = new ChooserSection(elem, id, name, this.checkboxHandler.bind(this)); - this._sections.push(newSection); - this._sections[id] = newSection; - } - } - this._basketContainer.UpdateBasketNumber(0); - - return result; -}; -ChooserComponent.prototype.checkboxHandler = function (sectionEvent) { - const chooserEvent = sectionEvent; - - chooserEvent.baseClass = this._baseClass; - chooserEvent.classification = this._classification; - chooserEvent.isValid = this.GetValidStatus(); - chooserEvent.total_count = this.GetTotalCount(); - chooserEvent.selected_count = this.GetSelectedCount(); - - const basketItem = { - groupId: chooserEvent.sectionCode, - captionText: chooserEvent.target.title, - code: chooserEvent.target.id, - }; - - if (chooserEvent.select_all) { - if (chooserEvent.target.checked) { - const newCollection = chooserEvent.section._allCheckboxes.map((item) => { - const newVal = { - groupId: chooserEvent.section._allCheckboxes[item].getAttribute('sectionid'), - code: chooserEvent.section._allCheckboxes[item].id, - captionText: chooserEvent.section._allCheckboxes[item].title, - }; - return newVal; - }); - this._basketContainer.AddItems(newCollection); - } else { - this._basketContainer.clearByGroupID.bind(this._basketContainer)(chooserEvent.sectionCode); - } - } else if (chooserEvent.target.checked) { - this._basketContainer.AddItem(basketItem); - } else { - this._basketContainer.RemoveItem(basketItem); - } - - this._basketContainer.UpdateBasketNumber(chooserEvent.selected_count); -}; -ChooserComponent.prototype.handleBasketRemove = function (groupID, value) { - const section = this._sections[groupID]; - section.uncheckItem.bind(section)(value); - - const chooserEvent = { - baseClass: this._baseClass, - classification: this._classification, - isValid: this.GetValidStatus(), - total_count: this.GetTotalCount(), - selected_count: this.GetSelectedCount(), - }; - - this._basketContainer.UpdateBasketNumber(chooserEvent.selected_count); -}; -ChooserComponent.prototype.GetValidStatus = function () { - let status = false; - for (let index = 0; !status && index < this._sections.length; index++) { - status = this._sections[index].IsSectionValid() || status; - } - return status; -}; -ChooserComponent.prototype.GetTotalCount = function () { - let total = 0; - - for (let index = 0; index < this._sections.length; index++) { - total += this._sections[index].GetTotalCount(); - } - - return total; -}; -ChooserComponent.prototype.GetSelectedCount = function () { - let total = 0; - - for (let index = 0; index < this._sections.length; index++) { - total += this._sections[index].GetSelectedCount(); - } - - return total; -}; -ChooserComponent.prototype.PrimeBasket = function () { - let selectedItems = []; - - if (this.GetSelectedCount() > 0) { - for (let index = 0; index < this._sections.length; index++) { - const sectionItem = this._sections[index]; - const mappedItems = []; - sectionItem._allCheckboxes.each(function () { - if (this.checked) { - const cb = this; - const newItem = { - code: cb.id, - groupId: sectionItem._sectionCode, - captionText: cb.title, - }; - mappedItems.push(newItem); - } - }); - selectedItems = selectedItems.concat(mappedItems); - } - this._basketContainer.AddItems(selectedItems); - this._basketContainer.UpdateBasketNumber(selectedItems.length); - } -}; - -function ChooserSection($parentSection, sectionCode, sectionName, checkboxHandler) { - this._parentElementName = $parentSection; - this._sectionName = sectionName; - this._sectionCode = sectionCode; - this._allCheckboxes = []; - this._allHandlerCheckbox = null; - this._selectAllCheckboxCount = 0; - this.init(checkboxHandler); -} - -ChooserSection.prototype.init = function (checkboxCallback) { - this._allCheckboxes = (this._parentElementName).find("input[type='checkbox']").filter("input[name!='section-checkbox_select_all']"); - this._allHandlerCheckbox = $(this._parentElementName).find("input[name='section-checkbox_select_all']"); - this.setAllHandlerCheckbox(this.GetSelectedCount(), this.GetTotalCount()); - this.connectCheckboxes(checkboxCallback); -}; -ChooserSection.prototype.connectCheckboxes = function (callback) { - const $self = this; - $(this._allCheckboxes).on('click', (e) => { - $self.checkboxHandler(e, callback); - }); - $(this._allHandlerCheckbox).on('click', (e) => { - $self.checkboxHandler(e, callback); - }); -}; -ChooserSection.prototype.uncheckItem = function (value) { - const target = this._allCheckboxes.filter((item) => this._allCheckboxes[item].id == value); - $(target).prop('checked', false); - if (this._allHandlerCheckbox) { - this._allHandlerCheckbox.prop('checked', false); - } -}; -ChooserSection.prototype.IsSectionValid = function () { - return this._allCheckboxes.filter(':checked').length > 0; -}; -ChooserSection.prototype.GetTotalCount = function () { - return this._allCheckboxes.length - this._selectAllCheckboxCount; -}; -ChooserSection.prototype.GetSelectedCount = function () { - return this._allCheckboxes.filter(':checked').length; -}; -ChooserSection.prototype.checkboxHandler = function (e, callback) { - const sectionEvent = { - section: this, - target: e.target, - targetid: e.target.id, - targetname: e.target.name, - sectionName: this._sectionName, - sectionCode: this._sectionCode, - section_total_count: undefined, - section_selected_count: undefined, - select_all: false, - }; - if (sectionEvent.targetid.endsWith('_all')) { - this._allCheckboxes.prop('checked', sectionEvent.target.checked); - sectionEvent.select_all = true; - } - sectionEvent.section_total_count = this.GetTotalCount(); - sectionEvent.section_selected_count = this.GetSelectedCount(); - - this.setAllHandlerCheckbox(sectionEvent.section_selected_count, sectionEvent.section_total_count); - - callback(sectionEvent); -}; -ChooserSection.prototype.setAllHandlerCheckbox = function (selectedCount, totalCount) { - if (selectedCount == totalCount) { - this._allHandlerCheckbox.prop('checked', true); - } else { - this._allHandlerCheckbox.prop('checked', false); - } -}; - -function BasketComponent(baseClass, classification, jqueryObject, removeHandler) { - this._baseClass = baseClass; - this._classification = classification; - this.jqueryObject = jqueryObject; - this.jqRemoveAll = null; - this.list = null; - this.removeAllBtn = null; - this.onRemove = removeHandler || this.onRemove; - this.init(); -} - -BasketComponent.prototype.init = function () { - this.list = this.jqueryObject.find('ul'); - - this.jqRemoveAll = this.jqueryObject.find('a.remove-link'); - if (this.jqRemoveAll.length > 0) { - this.jqRemoveAll.on('click', this.RemoveAll.bind(this)); - } -}; -BasketComponent.prototype.AddItems = function (itemsToAdd) { - for (let index = 0; index < itemsToAdd.length; index++) { - this.AddItem(itemsToAdd[index]); - } -}; -BasketComponent.prototype.AddItem = function (itemToAdd) { - const selectedID = `${itemToAdd.code}_basket`; - const removeLinkID = `${itemToAdd.code}_removeLink`; - - const newLI = `
  • ` - + '
    ' - + `Remove` - + '
    ' - + `
    ${itemToAdd.captionText}
  • `; - - if (this.list) { - if ($(this.list).find(`li#${selectedID}`).length === 0) { - $(this.list).append(newLI); - - $(`#${removeLinkID}`).on('click', (e) => { - e.preventDefault(); - this.removeSelectedItem(e.target.getAttribute('groupid'), e.target.id.replace('_removeLink', '')); - this.onRemove(e.target.getAttribute('groupid'), e.target.id.replace('_removeLink', '')); - }); - } - } -}; -BasketComponent.prototype.clear = function () { - this.list[0].innerHTML = ''; - this.UpdateBasketNumber(0); -}; -BasketComponent.prototype.clearByGroupID = function (groupID) { - let item = null; - const collection = this.list.find(`li[groupid="${groupID}"]`); - for (let index = 0; index < collection.length && (item = collection[index]) != null; index++) { - this.removeSelectedItem('', item.id.replace('_basket', '')); - } -}; -BasketComponent.prototype.removeSelectedItem = function (groupID, selectedID) { - $(`li#${selectedID}_basket`).remove(); -}; -BasketComponent.prototype.RemoveAll = function (e) { - let item = null; - const collection = this.list.find('li'); - for (let index = 0; index < collection.length && (item = collection[index]) != null; index++) { - this.removeSelectedItem('', item.id.replace('_basket', '')); - this.onRemove(item.getAttribute('groupid'), item.id.replace('_basket', '')); - } - e.preventDefault(); -}; -BasketComponent.prototype.RemoveItem = function (itemToRemove) { - this.removeSelectedItem(itemToRemove.groupId, itemToRemove.code); -}; -BasketComponent.prototype.onRemove = function (sectionID, itemValue) { - console.log(`Removing ${sectionID}.${itemValue}`); -}; -BasketComponent.prototype.UpdateBasketNumber = function (count) { - const selectedCount = $(`#selected-${this._classification}-count`); - const selectedParent = selectedCount.parent(); - - if (count < 2) { - this.jqRemoveAll.hide(); - } else { - this.jqRemoveAll.show(); - } - if (selectedCount) { - selectedCount.text(count); - } - - if (selectedParent && selectedParent.data('txt01')) { - if (count === 0) { - selectedCount.text(''); - selectedCount.next().text(selectedParent.data('txt02')); - } else if (count === 1) { - $(`#selected-${this._classification}-count`).next().text(selectedParent.data('txt03')); - } else { - $(`#selected-${this._classification}-count`).next().text(selectedParent.data('txt01')); - } - } -}; diff --git a/app/assets/javascripts/facilities-management/details/contract-extensions.js b/app/assets/javascripts/facilities-management/details/contract-extensions.js deleted file mode 100644 index 60489bfeaf..0000000000 --- a/app/assets/javascripts/facilities-management/details/contract-extensions.js +++ /dev/null @@ -1,272 +0,0 @@ -function contractPeriod(framework) { - const pagePeriods = { - totalContractPeriod: 0, - - init() { - this.$initialCallOffPeriodYears = $(`#facilities_management_${framework}_procurement_initial_call_off_period_years`); - this.$initialCallOffPeriodMonths = $(`#facilities_management_${framework}_procurement_initial_call_off_period_months`); - this.$mobilisationPeriodRequiredTrue = $(`#facilities_management_${framework}_procurement_mobilisation_period_required_true`); - this.$mobilisationPeriod = $(`#facilities_management_${framework}_procurement_mobilisation_period`); - this.$extensionsRequiredTrue = $(`#facilities_management_${framework}_procurement_extensions_required_true`); - }, - - callOffPeriodYears() { - return parseInt(this.$initialCallOffPeriodYears.val(), 10) * 156; - }, - - callOffPeriodMonths() { - return parseInt(this.$initialCallOffPeriodMonths.val(), 10) * 13; - }, - - mobilisationPeriodChecked() { - return this.$mobilisationPeriodRequiredTrue.is(':checked'); - }, - - mobilisationPeriod() { - return parseInt(this.$mobilisationPeriod.val(), 10) * 3; - }, - - extensionChecked() { - return this.$extensionsRequiredTrue.is(':checked'); - }, - - extensionYears(extension) { - return parseInt($(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_years`).val(), 10) * 156; - }, - - extensionMonths(extension) { - return parseInt($(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_months`).val(), 10) * 13; - }, - - yearsAndMonthsInomplete(years, months) { - return Number.isNaN(years) || Number.isNaN(months) || (years + months) === 0; - }, - - allPeriodInputsComplete() { - if (this.yearsAndMonthsInomplete(this.callOffPeriodYears(), this.callOffPeriodMonths())) return false; - - if (this.mobilisationPeriodChecked() && !(this.mobilisationPeriod() > 0)) return false; - - let extensionsCompleted = true; - - if (this.extensionChecked()) { - for (let extension = 0; extension <= 3; extension++) { - if ($(`#extension-${extension}-container`).hasClass('govuk-visually-hidden')) break; - - extensionsCompleted = !this.yearsAndMonthsInomplete(this.extensionYears(extension), this.extensionMonths(extension)); - - if (!extensionsCompleted) break; - } - } - - return extensionsCompleted; - }, - - calculateTotalContractPeriod() { - let totalPeriod = 0; - - totalPeriod += this.callOffPeriodYears() + this.callOffPeriodMonths(); - - totalPeriod += this.mobilisationPeriodChecked() ? this.mobilisationPeriod() : 0; - - if (this.extensionChecked()) { - for (let extension = 0; extension <= 3; extension++) { - if ($(`#extension-${extension}-container`).hasClass('govuk-visually-hidden')) break; - - totalPeriod += this.extensionYears(extension) + this.extensionMonths(extension); - } - } - - this.totalContractPeriod = totalPeriod; - }, - - totalTimeRemaining() { - return 1560 - this.totalContractPeriod; - }, - - timeRemaining() { - const totalTimeRemaining = this.totalTimeRemaining(); - - const years = Math.floor(totalTimeRemaining / 156); - const months = Math.floor((totalTimeRemaining % 156) / 13); - - return [years, months]; - }, - }; - - const extensionPeriods = { - showExtensionPeriod(extension) { - $(`#extension-${extension}-container`).removeClass('govuk-visually-hidden'); - $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_years`).attr('tabindex', 0); - $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_months`).attr('tabindex', 0); - $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_extension_required`).val('true'); - this.showRemoveButton(extension); - }, - - hideExtensionPeriod(extension) { - $(`#extension-${extension}-container`).addClass('govuk-visually-hidden'); - this.resetInput(extension, 'years'); - this.resetInput(extension, 'months'); - $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_extension_required`).val('false'); - $(`#extension-${extension}-container .govuk-error-message`).each((_, errorMessage) => { - $(errorMessage).remove(); - }); - this.hideRemoveButton(extension); - }, - - hideRemoveButton(extension) { - $(`#extension-${extension}-remove-button`).addClass('govuk-visually-hidden'); - $(`#extension-${extension}-remove-button`).attr('tabindex', -1); - }, - - showRemoveButton(extension) { - $(`#extension-${extension}-remove-button`).removeClass('govuk-visually-hidden'); - $(`#extension-${extension}-remove-button`).attr('tabindex', 0); - }, - - resetInput(extension, attribute) { - const element = $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_${attribute}`); - - element.attr('tabindex', -1); - element.val(''); - element.removeClass('govuk-input--error'); - }, - }; - - const addExtensionPeriodButton = { - addExtensionButton: $('#add-contract-extension-button'), - - ableToAddPeriod() { - if (this.forthExtensionRequired()) return false; - if (!pagePeriods.allPeriodInputsComplete()) return false; - - pagePeriods.calculateTotalContractPeriod(); - if (this.noTimePeriodLeftToAdd()) return false; - - return true; - }, - - updateButtonVisibility() { - if (!pagePeriods.extensionChecked() || this.forthExtensionRequired() || this.noTimePeriodLeftToAdd()) { - this.hideButton(); - } else { - this.showButton(); - } - }, - - updateButtonText() { - this.addExtensionButton.text(this.getButtonText()); - }, - - getButtonText() { - const timeRemainingParts = pagePeriods.timeRemaining(); - const years = timeRemainingParts[0]; - const months = timeRemainingParts[1]; - - let text = 'Add another extension period ('; - - if (years > 0) text += `${years} year`; - if (years > 1) text += 's'; - if (years > 0 && months > 0) text += ' and '; - if (months > 0) text += `${months} month`; - if (months > 1) text += 's'; - - return `${text} remaining)`; - }, - - forthExtensionRequired() { - return $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_3_extension_required`).val() === 'true'; - }, - - noTimePeriodLeftToAdd() { - return pagePeriods.totalTimeRemaining() < 13; - }, - - hideButton() { - this.addExtensionButton.addClass('govuk-visually-hidden'); - this.addExtensionButton.attr('tabindex', -1); - }, - - showButton() { - this.addExtensionButton.removeClass('govuk-visually-hidden'); - this.addExtensionButton.attr('tabindex', 0); - }, - - updateButtonState() { - if (pagePeriods.allPeriodInputsComplete()) { - pagePeriods.calculateTotalContractPeriod(); - this.updateButtonVisibility(); - this.updateButtonText(); - } - }, - }; - - pagePeriods.init(); - - $(`#facilities_management_${framework}_procurement_extensions_required_true`).on('click', () => { - extensionPeriods.showExtensionPeriod(0); - addExtensionPeriodButton.hideButton(); - if (addExtensionPeriodButton.ableToAddPeriod()) addExtensionPeriodButton.showButton(); - }); - - $(`#facilities_management_${framework}_procurement_extensions_required_false`).on('click', () => { - $('.extension-container').each((extension) => { - extensionPeriods.hideExtensionPeriod(extension); - }); - addExtensionPeriodButton.hideButton(); - }); - - $('.extension-remove-button').each((_, button) => { - $(button).on('click', (e) => { - e.preventDefault(); - - extensionPeriods.hideExtensionPeriod($(button).attr('data-extension')); - extensionPeriods.showRemoveButton($(button).attr('data-extension') - 1); - - if (addExtensionPeriodButton.ableToAddPeriod()) { - addExtensionPeriodButton.updateButtonVisibility(); - addExtensionPeriodButton.updateButtonText(); - } - }); - }); - - $('#add-contract-extension-button').on('click', (e) => { - e.preventDefault(); - - if (addExtensionPeriodButton.ableToAddPeriod()) { - const nextExtension = $($('.extension-container.govuk-visually-hidden').get(0)).attr('data-extension'); - extensionPeriods.showExtensionPeriod(nextExtension); - extensionPeriods.hideRemoveButton(nextExtension - 1); - - addExtensionPeriodButton.updateButtonVisibility(); - addExtensionPeriodButton.updateButtonText(); - } - }); - - $('.period-input').on('keyup', () => { - addExtensionPeriodButton.updateButtonState(); - }); - - $(`#facilities_management_${framework}_procurement_mobilisation_period_required_true`).on('click', () => { - addExtensionPeriodButton.updateButtonState(); - }); - - $(`#facilities_management_${framework}_procurement_mobilisation_period_required_false`).on('click', () => { - addExtensionPeriodButton.updateButtonState(); - }); - - if (addExtensionPeriodButton.ableToAddPeriod()) { - addExtensionPeriodButton.updateButtonText(); - addExtensionPeriodButton.updateButtonVisibility(); - } else { - addExtensionPeriodButton.hideButton(); - } -} - -$(() => { - const $callOffExtensionsContainer = $('#call-off-extensions'); - - if ($callOffExtensionsContainer.length > 0) { - contractPeriod($callOffExtensionsContainer.data().framework); - } -}); diff --git a/app/assets/javascripts/facilities-management/fm-contact-detail.js b/app/assets/javascripts/facilities-management/fm-contact-detail.js deleted file mode 100644 index 05e8cfbf2b..0000000000 --- a/app/assets/javascripts/facilities-management/fm-contact-detail.js +++ /dev/null @@ -1,80 +0,0 @@ -$(() => { - const contactDetail = { - makeElementName() { - const modelName = $('#object_name').val(); - - const nameElem = `#${modelName}_name`; - const fullNameElem = `#${modelName}_full_name`; - const jobTitleElem = `#${modelName}_job_title`; - const emailElem = `#${modelName}_email`; - const telephoneNumberElem = `#${modelName}_telephone_number`; - const orgNameElem = `#${modelName}_organisation_name`; - - return { - name: nameElem, fullName: fullNameElem, jobTitle: jobTitleElem, email: emailElem, telephoneNumber: telephoneNumberElem, orgName: orgNameElem, - }; - }, - - fillInDetails() { - const contactDetailsFormElements = this.makeElementName(); - - $(contactDetailsFormElements.name).val(window.sessionStorage.contactDetailsName); - $(contactDetailsFormElements.fullName).val(window.sessionStorage.contactDetailsfullName); - $(contactDetailsFormElements.jobTitle).val(window.sessionStorage.contactDetailsJobTitle); - $(contactDetailsFormElements.email).val(window.sessionStorage.contactDetailsEmail); - $(contactDetailsFormElements.telephoneNumber).val(window.sessionStorage.contactDetailsTelephoneNumber); - $(contactDetailsFormElements.orgName).val(window.sessionStorage.contactDetailsOrgName); - - $('#facilities_management_buyer_detail_central_government_true').prop('checked', window.sessionStorage.contactDetailsSectorTrue === 'true'); - $('#facilities_management_buyer_detail_central_government_false').prop('checked', window.sessionStorage.contactDetailsSectorFalse === 'true'); - }, - - getDetails() { - const contactDetailsFormElements = this.makeElementName(); - - window.sessionStorage.contactDetailsName = $(contactDetailsFormElements.name).val(); - window.sessionStorage.contactDetailsfullName = $(contactDetailsFormElements.fullName).val(); - window.sessionStorage.contactDetailsJobTitle = $(contactDetailsFormElements.jobTitle).val(); - window.sessionStorage.contactDetailsEmail = $(contactDetailsFormElements.email).val(); - window.sessionStorage.contactDetailsTelephoneNumber = $(contactDetailsFormElements.telephoneNumber).val(); - window.sessionStorage.contactDetailsOrgName = $(contactDetailsFormElements.orgName).val(); - window.sessionStorage.contactDetailsSectorTrue = $('#facilities_management_buyer_detail_central_government_true').is(':checked'); - window.sessionStorage.contactDetailsSectorFalse = $('#facilities_management_buyer_detail_central_government_false').is(':checked'); - - window.sessionStorage.contactDetails = true; - }, - - removeDetails() { - window.sessionStorage.removeItem('contactDetails'); - window.sessionStorage.removeItem('contactDetailsName'); - window.sessionStorage.removeItem('contactDetailsfullName'); - window.sessionStorage.removeItem('contactDetailsJobTitle'); - window.sessionStorage.removeItem('contactDetailsEmail'); - window.sessionStorage.removeItem('contactDetailsTelephoneNumber'); - window.sessionStorage.removeItem('contactDetailsOrgName'); - }, - }; - - if (typeof (Storage) !== 'undefined') { - const formContactDetails = $('#edit-contact-detail'); - const formContactDetailsAddress = $('#edit-contact-detail-address'); - - if (formContactDetails.length && window.sessionStorage.contactDetails) { - contactDetail.fillInDetails(); - } - - if (formContactDetails.length) { - $('#cant-find-address-link').on('click', () => { - contactDetail.getDetails(); - }); - } else if (!formContactDetailsAddress.length) { - contactDetail.removeDetails(); - } - - if (formContactDetails.length) { - $(document.querySelector('input[type="submit"]')).on('click', () => { - contactDetail.removeDetails(); - }); - } - } -}); diff --git a/app/assets/javascripts/facilities-management/fm-find-address.js b/app/assets/javascripts/facilities-management/fm-find-address.js deleted file mode 100644 index 9b05738f33..0000000000 --- a/app/assets/javascripts/facilities-management/fm-find-address.js +++ /dev/null @@ -1,402 +0,0 @@ -function FindAddressComponent() { - this.init(); -} - -FindAddressComponent.prototype.init = function () { - this.postcodeSearch = $('#postcode-search'); - this.postcodeChange = $('#postcode-change'); - this.selectAnAddress = $('#select-an-address'); - this.fullAddress = $('#full-address'); - - this.findAddressBtn = $('#find-address-button'); - this.searchAddress = $('.postcode-entry').first(); - this.changePostcodeLink = $('#change-input-1'); - this.changeAddressLink = $('#change-input-2'); - - this.addressDropDown = $('#address-results-container'); - - this.postcodeText = $('#postcode-on-view'); - this.addressText = $('#address-text'); - - this.objectName = $('#object_name').val(); - this.postcodeName = $('#postcode_name').val(); - - this.regionContainterPresent = $("[data-module='find-region']").length > 0; - - this.setupSelectBoxesForAddress(); - this.setupEventListenersForAddress(); - - if (this.regionContainterPresent) { - this.changeRegionLink = $('#change-input-3'); - this.regionDropDown = $('#regions-container'); - this.regionText = $('#region-text'); - this.fullRegion = $('#full-region'); - this.selectARegion = $('#select-a-region'); - - this.setupSelectBoxesForRegions(); - this.setupEventListenersForRegions(); - } -}; - -FindAddressComponent.prototype.setupSelectBoxesForAddress = function () { - const selectAddress = this.selectAddress.bind(this); - - this.addressDropDown.on('blur', (e) => { - e.preventDefault(); - selectAddress(); - }); - - if (!(/Windows/.test(navigator.userAgent))) { - this.addressDropDown.on('change', selectAddress); - } else { - this.addressDropDown.on('click', function () { - if (this.selectedIndex > 0) { - selectAddress(); - } - }); - this.addressDropDown.on('keypress', function (e) { - if (e.keyCode === 13 && this.selectedIndex > 0) { - e.preventDefault(); - e.stopPropagation(); - selectAddress(); - } - }); - } -}; - -FindAddressComponent.prototype.setupSelectBoxesForRegions = function () { - const selectRegion = this.selectRegion.bind(this); - - this.regionDropDown.on('blur', (e) => { - e.preventDefault(); - selectRegion(); - }); - - if (!(/Windows/.test(navigator.userAgent))) { - this.regionDropDown.on('change', selectRegion); - } else { - this.regionDropDown.on('click', function () { - if (this.selectedIndex > 0) { - selectRegion(); - } - }); - this.regionDropDown.on('keypress', function (e) { - if (e.keyCode === 13 && this.selectedIndex > 0) { - e.preventDefault(); - e.stopPropagation(); - selectRegion(); - } - }); - } -}; - -FindAddressComponent.prototype.setupEventListenersForAddress = function () { - const module = this; - - this.findAddressBtn.on('click', module.lookupInput.bind(this)); - - this.searchAddress.on('keypress', (e) => { - if (e.keyCode === 13) { - module.lookupInput(e); - } - }); - - this.changePostcodeLink.on('click', this.changePostcode.bind(this)); - this.changeAddressLink.on('click', this.changeAddress.bind(this)); - - $('#new_facilities_management_building').on('submit', () => { - module.errorShow(false, module.searchAddress, module.postcodeName, 'input'); - }); -}; - -FindAddressComponent.prototype.setupEventListenersForRegions = function () { - this.changeRegionLink.on('click', this.changeRegion.bind(this)); -}; - -FindAddressComponent.prototype.lookupInput = function (e) { - e.preventDefault(); - const module = this; - - this.errorShow(false, module.searchAddress, this.postcodeName, 'input'); - - const input = common.destructurePostCode(this.searchAddress.val()); - - if (input.valid) { - module.findAddress(input.fullPostcode); - } else { - module.errorShow(true, module.searchAddress, this.postcodeName, 'input'); - } -}; - -FindAddressComponent.prototype.errorShow = function (show, input, attribute, inputError) { - if (show) { - anyArbitraryName.global_formValidators[0].toggleError($(input), true, 'invalid'); - } else { - $(`#${attribute}-form-group`).removeClass('govuk-form-group--error'); - $(`span[id='${attribute}-error']`).remove(); - $(`#error_${this.objectName}_${attribute}`).remove(); - $(`label[id='${attribute}-error'] > span`).addClass('govuk-visually-hidden'); - input.removeClass(`govuk-${inputError}--error`); - } -}; - -FindAddressComponent.prototype.findAddress = function (postcode) { - const module = this; - const validPostcode = this.normalisePostcode(postcode); - const url = encodeURI(`/api/v2/postcodes/${validPostcode}`); - - $.ajax({ - type: 'GET', - url, - data: $(this).serialize(), - dataType: 'json', - success(data) { - module.processAddress(data.result, postcode); - }, - error() { - module.processAddress([], postcode); - }, - }); -}; - -FindAddressComponent.prototype.normalisePostcode = function (postcode) { - return postcode.toUpperCase().replace(/\s/g, ''); -}; - -FindAddressComponent.prototype.processAddress = function (result, postcode) { - const module = this; - - this.addressDropDown.empty(); - this.setBlankOption(this.addressDropDown, result.length, 'Please select an address'); - - if (result.length > 0) { - module.addAddressOptions(result); - } - - this.postcodeText.text(postcode); - this.updateView(2); -}; - -FindAddressComponent.prototype.addAddressOptions = function (addresses) { - addresses.forEach((address) => { - const newOption = document.createElement('option'); - newOption.value = address.summary_line; - newOption.innerText = address.summary_line; - newOption.dataset.address_line_1 = address.address_line_1; - newOption.dataset.address_line_2 = address.address_line_2; - newOption.dataset.address_town = address.address_town; - newOption.dataset.address_postcode = address.address_postcode; - this.addressDropDown.append(newOption); - }); -}; - -FindAddressComponent.prototype.setBlankOption = function (search, results, dropDownText) { - const module = this; - - if (results === 0) { - let text = search.data('withdata-text-plural'); - text = `0 ${text}`; - - module.setBlankOptionText(search, text, text); - } else if (results === 1) { - let text = search.data('withdata-text-single'); - text = `${results} ${text}`; - - module.setBlankOptionText(search, text, dropDownText); - } else { - let text = search.data('withdata-text-plural'); - text = `${results} ${text}`; - - module.setBlankOptionText(search, text, dropDownText); - } -}; - -FindAddressComponent.prototype.setBlankOptionText = function (search, optionalText, text) { - const option = document.createElement('option'); - option.appendChild(document.createTextNode(text)); - - search.prepend(` `); - search.append(option); -}; - -FindAddressComponent.prototype.selectAddress = function () { - const selectedOption = this.addressDropDown.find('option:selected'); - - if (selectedOption.index() <= 1) return; - - this.errorShow(false, this.addressDropDown, 'base', 'select'); - - $('#address-line-1').val(selectedOption.data('address_line_1')); - $('#address-line-2').val(selectedOption.data('address_line_2')); - $('#address-town').val(selectedOption.data('address_town')); - this.addressText.text(`${selectedOption.text()} ${selectedOption.data('address_postcode')}`); - - if (this.regionContainterPresent) { - this.findRegion(); - } else { - this.updateView(5); - } -}; - -FindAddressComponent.prototype.findRegion = function () { - const module = this; - const postcode = this.postcodeText.text(); - const validPostcode = this.normalisePostcode(postcode); - const url = encodeURI(`/api/v2/find-region-postcode/${validPostcode}`); - - $.ajax({ - type: 'GET', - url, - data: $(this).serialize(), - dataType: 'json', - success(data) { - module.processRegion(data.result); - }, - error() { - module.processRegion([]); - }, - }); -}; - -FindAddressComponent.prototype.processRegion = function (regions) { - this.regionDropDown.empty(); - this.setBlankOption(this.regionDropDown, regions.length, 'Please select a region'); - - if (regions.length > 0) { - regions.forEach((region) => { - const newOption = document.createElement('option'); - newOption.value = region.code; - newOption.innerText = region.region; - newOption.dataset.address_region = region.region; - newOption.dataset.address_region_code = region.code; - this.regionDropDown.append(newOption); - }); - } - - if (regions.length === 1) { - this.selectOneRegion(); - this.updateView(5); - } else { - this.updateView(3); - } -}; - -FindAddressComponent.prototype.selectOneRegion = function () { - this.regionDropDown.find('option:eq(1)').attr('selected', 'selected'); - this.selectRegion(); -}; - -FindAddressComponent.prototype.selectRegion = function () { - const selectedOption = this.regionDropDown.find('option:selected'); - - if (selectedOption.index() <= 1) return; - - this.errorShow(false, this.regionDropDown, 'address_region', 'select'); - - $('#address-region').val(selectedOption.data('address_region')); - $('#address-region-code').val(selectedOption.data('address_region_code')); - this.regionText.text(selectedOption.data('address_region')); - - this.updateView(4); -}; - -FindAddressComponent.prototype.changePostcode = function (e) { - e.preventDefault(); - this.errorShow(false, this.addressDropDown, 'base', 'select'); - this.removeAddress(); - this.removeRegion(); - this.updateView(1); -}; - -FindAddressComponent.prototype.changeAddress = function (e) { - e.preventDefault(); - this.errorShow(false, this.addressDropDown, 'base', 'select'); - this.removeAddress(); - this.removeRegion(); - this.updateView(1); -}; - -FindAddressComponent.prototype.changeRegion = function (e) { - e.preventDefault(); - this.errorShow(false, this.regionDropDown, 'address_region', 'select'); - this.removeRegion(); - this.updateView(3); -}; - -FindAddressComponent.prototype.removeAddress = function () { - $('#address-line-1').val(''); - $('#address-line-2').val(''); - $('#address-town').val(''); - $('#address-county').val(''); -}; - -FindAddressComponent.prototype.removeRegion = function () { - if (!this.regionContainterPresent) return; - - this.regionDropDown.find('option:selected').prop('selected', false); - $('#address-region').val(''); - $('#address-region-code').val(''); -}; - -FindAddressComponent.prototype.updateView = function (state) { - this.showOrHideInputs(state === 1, this.postcodeSearch); - this.showOrHideInputs(state === 1, this.findAddressBtn); - - this.showOrHideInputs(state === 2, this.postcodeChange); - this.showOrHideInputs(state === 2, this.selectAnAddress); - - this.showOrHideInputs([3, 4, 5].indexOf(state) !== -1, this.fullAddress); - - if (this.regionContainterPresent) { - this.showOrHideInputs(state === 3, this.selectARegion); - - this.showOrHideInputs([4, 5].indexOf(state) !== -1, this.fullRegion); - - this.showOrHideInputs(state === 4, this.changeRegionLink); - } - - this.updateFocus(state); -}; - -FindAddressComponent.prototype.updateFocus = function (state) { - switch (state) { - case 1: - this.searchAddress.focus(); - break; - case 2: - this.addressDropDown.focus(); - break; - case 3: - if (this.regionContainterPresent) this.regionDropDown.focus(); - break; - case 4: - if (this.regionContainterPresent) this.changeRegionLink.focus(); - break; - case 5: - this.changeAddressLink.focus(); - break; - default: - break; - } -}; - -FindAddressComponent.prototype.showOrHideInputs = function (show, section) { - let tabindex; - - if (show) { - section.removeClass('govuk-visually-hidden'); - tabindex = 0; - } else { - section.addClass('govuk-visually-hidden'); - tabindex = -1; - } - - if (section.attr('tabindex')) section.attr('tabIndex', tabindex); - section.find('[tabindex]').attr('tabIndex', tabindex); -}; - -$(() => { - if (document.querySelectorAll("[data-module='find-address']").length) { - new FindAddressComponent(); - } -}); diff --git a/app/assets/javascripts/facilities-management/fm-number-sections.js b/app/assets/javascripts/facilities-management/fm-number-sections.js deleted file mode 100644 index 87f3c2e6ea..0000000000 --- a/app/assets/javascripts/facilities-management/fm-number-sections.js +++ /dev/null @@ -1,47 +0,0 @@ -const numberInput = { - input: $('.ccs-number-field'), - form: $('form'), - - updateNumberWithCommas() { - const number = this.input.val(); - const numberString = number.toString().replace(/,/g, ''); - - this.input.val(numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ',')); - }, - - updateNumberWithoutCommas() { - const number = this.input.val(); - - this.input.val(number.toString().replace(/,/g, '')); - }, - - showNumberWithCommas() { - this.updateNumberWithCommas(); - - this.input.on('keyup', () => { - this.updateNumberWithCommas(); - }); - - this.form.on('submit', () => { - this.updateNumberWithoutCommas(); - }); - }, - - limitInputToInteger() { - $('.ccs-integer-field').on('keypress', (e) => { - if ((e.key < '0' || e.key > '9')) { - e.preventDefault(); - } - }); - }, -}; - -$(() => { - if ($('.ccs-number-field').length) { - numberInput.showNumberWithCommas(); - } - - if ($('.ccs-integer-field').length) { - numberInput.limitInputToInteger(); - } -}); diff --git a/app/assets/javascripts/facilities-management/fm-procurement-building.js b/app/assets/javascripts/facilities-management/fm-procurement-building.js deleted file mode 100644 index 3101d4cd01..0000000000 --- a/app/assets/javascripts/facilities-management/fm-procurement-building.js +++ /dev/null @@ -1,49 +0,0 @@ -const confirmBuildingRegion = { - $regionDropDown: $('#facilities_management_building_address_region'), - $changeRegion: $('#change-region'), - - init() { - this.$regionDropDown.on('change', () => { - this.selectRegion(); - }); - - this.$changeRegion.on('click', (event) => { - this.changeRegion(event); - }); - }, - - selectRegion() { - const value = this.$regionDropDown.find(':selected').text(); - - if (value) { - this.$regionDropDown.attr('tabIndex', -1); - this.$changeRegion.attr('tabIndex', 0); - this.$changeRegion.get(0).focus(); - - $('.govuk-error-summary').hide(); - $('#address_region-error').hide(); - $('#address_region-form-group').removeClass('govuk-form-group--error'); - $('#building-region').text(value); - $('#select-region').hide(); - $('#region-selection').show(); - } - }, - - changeRegion(event) { - event.preventDefault(); - - this.$regionDropDown.attr('tabIndex', 0); - this.$changeRegion.attr('tabIndex', -1); - this.$regionDropDown.get(0).focus(); - - this.$regionDropDown.prop('selectedIndex', 0); - $('#region-selection').hide(); - $('#select-region').show(); - }, -}; - -$(() => { - if ($('#building-missing-region').length) { - confirmBuildingRegion.init(); - } -}); diff --git a/app/assets/javascripts/facilities-management/fm-table-filter.js b/app/assets/javascripts/facilities-management/fm-table-filter.js deleted file mode 100644 index c8bb045f92..0000000000 --- a/app/assets/javascripts/facilities-management/fm-table-filter.js +++ /dev/null @@ -1,24 +0,0 @@ -function filterTable() { - // Declare variables - let row; - let rowValue; - const filter = $('#fm-table-filter-input').val().toUpperCase(); - const tableRows = $('#fm-table-filter').find('tbody').find('tr'); - const column = $('#fm-table-filter-input').attr('data-column'); - - // Loop through all table rows, and hide those who don't match the search query - tableRows.each(function () { - row = $(this).children().get(column); - rowValue = $(row).prop('innerText'); - - if (rowValue && rowValue.toUpperCase().indexOf(filter) > -1) { - $(this).show(); - } else { - $(this).hide(); - } - }); -} - -$(() => { - $('#fm-table-filter-input').on('keyup', () => { filterTable(); }); -}); diff --git a/app/assets/javascripts/facilities-management/fm-upload-progress.js b/app/assets/javascripts/facilities-management/fm-upload-progress.js deleted file mode 100644 index 359e596846..0000000000 --- a/app/assets/javascripts/facilities-management/fm-upload-progress.js +++ /dev/null @@ -1,66 +0,0 @@ -const uploadFileImport = { - continue: true, - currentState: 'in_progress', - - init(stateToProgress, succeeded_state, failed_state) { - this.url = `${window.location.pathname}/progress`; - this.stateToProgress = stateToProgress; - this.succeeded_state = succeeded_state; - this.failed_state = failed_state; - - setTimeout(this.checkImportProgress, this.interval); - }, - - checkImportProgress() { - $.ajax({ - type: 'GET', - url: uploadFileImport.url, - data: $(this).serialize(), - dataType: 'json', - success(data) { - uploadFileImport.currentState = data.import_status; - }, - error() { - uploadFileImport.continue = false; - }, - complete() { - uploadFileImport.processImportStatus(); - }, - }); - }, - - processImportStatus() { - if (this.continue) { - $('#upload-import-progress').attr('style', `width: ${this.stateToProgress[this.currentState].progress}%`); - this.updateCurrentState(); - let continueFunction = this.checkImportProgress; - - if (this.currentState === this.succeeded_state || this.currentState === this.failed_state) { - $('#upload-import-progress').addClass(this.stateToProgress[this.currentState].colourClass); - continueFunction = this.processComplete; - } - - setTimeout(continueFunction, this.stateToProgress[this.currentState].wait); - } else { - this.processComplete(); - } - }, - - updateCurrentState() { - $('.ccs-upload-progress-container > div').each(this.updateShownStatus.bind(this)); - }, - - updateShownStatus(_, element) { - if ($(element).attr('id') === this.stateToProgress[this.currentState].state) { - $(element).attr('aria-current', true); - $(element).addClass('govuk-!-font-weight-bold'); - } else { - $(element).removeAttr('aria-current'); - $(element).removeClass('govuk-!-font-weight-bold'); - } - }, - - processComplete() { - window.location.reload(); - }, -}; diff --git a/app/assets/javascripts/facilities-management/rm3830/admin/fm-management-report-progress.js b/app/assets/javascripts/facilities-management/rm3830/admin/fm-management-report-progress.js deleted file mode 100644 index 6f050cb1df..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/admin/fm-management-report-progress.js +++ /dev/null @@ -1,49 +0,0 @@ -const managementReportProgress = { - continue: true, - currentState: 'generating_csv', - interval: 15000, - - init() { - this.statusURL = `${window.location.pathname}/progress`; - - setTimeout(this.checkImportProgress, this.interval); - }, - - checkImportProgress() { - $.ajax({ - type: 'GET', - url: managementReportProgress.statusURL, - data: $(this).serialize(), - dataType: 'json', - success(data) { - managementReportProgress.currentState = data.status; - }, - error() { - managementReportProgress.continue = false; - }, - complete() { - managementReportProgress.processImportStatus(); - }, - }); - }, - - processImportStatus() { - if (this.continue) { - if (this.currentState === 'completed') { - this.processComplete(); - } else { - setTimeout(this.checkImportProgress, this.interval); - } - } - }, - - processComplete() { - window.location.reload(); - }, -}; - -$(() => { - if ($('#management-report-status').length && $('.management-report-state-generating_csv').length) { - managementReportProgress.init(); - } -}); diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/choose-services-buildings.js b/app/assets/javascripts/facilities-management/rm3830/procurement/choose-services-buildings.js deleted file mode 100644 index b8c6f19b43..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/choose-services-buildings.js +++ /dev/null @@ -1,37 +0,0 @@ -const assignServicesToBuildings = { - init() { - this.checkAllSelected(); - this.initSelectAll(); - }, - - checkAllSelected() { - $('#box-all').prop('checked', $('input.procurement-building__input').length === $('input.procurement-building__input:checked').length); - }, - - checkAll(choice) { - $('.procurement-building__input').each(function () { - $(this).prop('checked', choice); - }); - $('#box-all').prop('checked', choice); - }, - - initSelectAll() { - const assignServicesToBuildingsObj = this; - - $('.govuk-checkboxes').each(function () { - $(this).on('click', () => { - assignServicesToBuildingsObj.checkAllSelected(); - }); - }); - - $('#box-all').on('click', () => { - assignServicesToBuildingsObj.checkAll($('.govuk-checkboxes').find('input.procurement-building__input').length > $('.govuk-checkboxes').find('input.procurement-building__input:checked').length); - }); - }, -}; - -$(() => { - if ($('.buildings_and_services').length && $('.ccs-select-all').length) { - assignServicesToBuildings.init(); - } -}); \ No newline at end of file diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/contract-details.js b/app/assets/javascripts/facilities-management/rm3830/procurement/contract-details.js deleted file mode 100644 index bc6f464709..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/contract-details.js +++ /dev/null @@ -1,21 +0,0 @@ -$(() => { - if ($('.govuk-details').length) { - const removeTabIndexOnLinks = (removeTabIndex) => { - $('.govuk-details__text a').each(function () { - removeTabIndex ? $(this).removeAttr('tabindex') : $(this).attr('tabindex', -1); - }); - }; - - const govukDetails = $('.govuk-details'); - - govukDetails.each((_, govukDetail) => { - const observer = new MutationObserver(() => { - removeTabIndexOnLinks(govukDetail.open); - }); - - const config = { attributes: true, childList: false, characterData: false }; - - observer.observe(govukDetail, config); - }); - } -}); diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/form-validation-component.js b/app/assets/javascripts/facilities-management/rm3830/procurement/form-validation-component.js deleted file mode 100644 index f737158e08..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/form-validation-component.js +++ /dev/null @@ -1,556 +0,0 @@ -function FormValidationComponent(formDOMObject, validationCallback, thisisspecial) { - if (undefined === thisisspecial) thisisspecial = false; - this.site_wide_turn_off = false; - - this.verify_connection_to_form = function (formDOMObject, requestedSpecialTreatment) { - let canConnect = false; - if (!this.site_wide_turn_off && formDOMObject != null && formDOMObject.formValidator == null) { - if ((requestedSpecialTreatment && formDOMObject.getAttribute('specialvalidation') === 'true') - || (!requestedSpecialTreatment && !formDOMObject.hasAttribute('specialvalidation')) - || (!requestedSpecialTreatment && formDOMObject.getAttribute('specialvalidation') !== 'true')) { - canConnect = true; - } - } - return canConnect; - }; - - this.connect_to_form = function (formDOMObject, validationCallback) { - this.form = formDOMObject; - this.form.formValidator = this; - this.validator = validationCallback.bind(this); - this.validationResult = true; - - this.bannerErrorContainer = $('[data-module="error-summary"]'); - - this.form.onsubmit = function (e) { - return this.validator(this.form.elements); - }.bind(this); - }; - - this.validateTheForm = function () { - return this.validator(this.form.elements); - }; - - this.initialise = function () { - this.validationResult = true; - }; - - this.isRadioOrCheckBox = function (jElem) { - return 'radio checkbox'.indexOf(jElem.prop('type')) > -1; - }; - - this.validateForm = function (formElements) { - let submitForm = this.validationResult = true; - this.clearBannerErrorList(); - this.toggleBannerError(false); - this.clearAllFieldErrors(); - - if (formElements !== undefined && formElements.length > 0) { - const elements = []; - const validated_radiocheckbox_elements = []; - for (let i = 0; i < formElements.length; i++) { - const element = formElements[i]; - if (element.hasAttribute('type') || element.hasAttribute('required') || element.hasAttribute('maxlength')) { - if (element.getAttribute('type') !== 'hidden') { - elements.push(element); - } - } - } - if (elements.length > 0) { - for (let index = 0; index < elements.length; index++) { - try { - const jElem = $(elements[index]); - if (jElem.length > 0) { - if (this.isRadioOrCheckBox(jElem) && jQuery.inArray(jElem.prop('name'), validated_radiocheckbox_elements) == -1) { - validated_radiocheckbox_elements.push(jElem.prop('name')); - if (jElem.prop('required')) { - if (this.form.elements[jElem.prop('name')].value === '') { - submitForm = submitForm && false; - this.toggleError(jElem, true, 'required'); - } else { - submitForm = submitForm && true; - this.toggleError(jElem, false, 'required'); - } - } - } - if (jElem.prop('required')) { - submitForm = submitForm && this.testError( - this.validationFunctions.required, - jElem, 'required', - ); - } - if (jElem.prop('maxlength') > 0) { - submitForm = submitForm && this.testError( - this.validationFunctions.maxlength, - jElem, 'maxlength', - ); - } - if (jElem.prop('type') && submitForm) { - const htmlAttributeValue = jElem[0].getAttribute('type'); - try { - for (const prop in this.validationFunctions.type) { - if (this.validationFunctions.type.hasOwnProperty(prop)) { - const fn = this.validationFunctions.type[prop]; - if (fn != null && htmlAttributeValue == prop) { - submitForm = submitForm && this.testError( - fn, jElem, prop, - ); - } - } - } - } catch (e) { - console.log(e); - } - } - if (jElem.prop('pattern') !== undefined && jElem.prop('pattern') !== '' && submitForm) { - submitForm = submitForm && this.testError( - this.validationFunctions.regex, - jElem, 'invalid', - ); - } - if (jElem.prop('min') !== undefined && jElem.prop('min') !== '' && submitForm) { - submitForm = submitForm && this.testError( - this.validationFunctions.min, - jElem, 'min', - ); - } - if (jElem.prop('max') !== undefined && jElem.prop('max') !== '' && submitForm) { - submitForm = submitForm && this.testError( - this.validationFunctions.max, - jElem, 'max', - ); - } - } - } catch (e) { - console.log(e); - } - - this.validationResult = this.validationResult && submitForm; - submitForm = true; - } - } - } - - if (!this.validationResult) { - this.toggleBannerError(true); - } - - if (formElements.preventsubmission === 'true') { - return false; - } - - return this.validationResult; - }; - this.validationFunctions = { - required(jQueryinputElem) { - const inputType = jQueryinputElem[0].type; - jQueryinputElem[0].type = 'text'; - const result = (`${jQueryinputElem.val()}` === ''); - jQueryinputElem[0].type = inputType; - return result; - }, - maxlength(jQueryinputElem) { - const maxLength = parseInt(jQueryinputElem.prop('maxlength')); - if (!isNaN(maxLength) && maxLength > 0) { - const inputType = jQueryinputElem[0].type; - jQueryinputElem[0].type = 'text'; - const result = ((`${jQueryinputElem.val()}`).length > maxLength); - jQueryinputElem[0].type = inputType; - return result; - } - return false; - }, - max(jQueryInputElem) { - const inputValue = Number(jQueryInputElem.val()); - const maxVal = parseInt(jQueryInputElem.prop('max')); - if (!isNaN(inputValue) && !isNaN(maxVal)) { - return inputValue > maxVal; - } - return false; - }, - min(jQueryInputElem) { - const inputValue = Number(jQueryInputElem.val()); - const minVal = parseInt(jQueryInputElem.prop('min')); - - if (!isNaN(inputValue) && !isNaN(minVal)) { - return inputValue < minVal; - } - - return false; - }, - range(jQueryInputElem) { - const inputValue = Number(jQueryInputElem.val()); - const maxVal = parseInt(jQueryInputElem.prop('max')); - const minVal = parseInt(jQueryInputElem.prop('min')); - if (!isNaN(inputValue) && !isNaN(maxVal)) { - return inputValue > maxVal; - } - if (!isNaN(inputValue) && !isNaN(minVal)) { - return inputValue < minVal; - } - - return false; - }, - regex(jQueryinputElem) { - const reg = new RegExp(jQueryinputElem.prop('pattern')); - return !reg.test(jQueryinputElem.val()); - }, - type: { - text(jQueryinputElem) { - return false; - }, - number(jQueryinputElem) { - const inputType = jQueryinputElem[0].type; - jQueryinputElem[0].type = 'text'; - - let result = jQueryinputElem.val() === '' || isNaN(Number(jQueryinputElem.val())); - if (!result && jQueryinputElem[0].getAttribute('step') === '1') { - result = ((jQueryinputElem.val() % 1) != 0); - } - jQueryinputElem[0].type = inputType; - return result; - }, - email(jQueryinputElem) { - const regEx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return !regEx.test(jQueryinputElem.val()); - }, - date(jQueryInputElem) { - const theDate = Date.parse(jQueryInputElem.val()); - return theDate === NaN; - }, - }, - }; - - this.clearAllFieldErrors = function () { - $(this.form).find('.govuk-input--error').removeClass('govuk-input--error'); - $(this.form).find('.govuk-select--error').removeClass('govuk-select--error'); - $(this.form).find('.govuk-form-group--error').removeClass('govuk-form-group--error'); - $(this.form).find('.govuk-error-message').addClass('govuk-visually-hidden'); - $(this.form).find('.govuk-error-message').css('display', 'none'); - $(this.form).find('label[class=govuk-error-message]').addClass('govuk-visually-hidden'); - $(this.form).find('label[class=govuk-error-message]').css('display', 'none'); - }; - - this.clearFieldErrors = function (jElem) { - let errorCollection = jElem.siblings('label[class=govuk-error-message]'); - errorCollection = errorCollection.add(jElem.closest('.govuk-form-group.govuk-form-group--error').find('label.govuk-error-message').not('.govuk-visually-hidden')); - let groupCollection = jElem.closest('.govuk-form-group .govuk-form-group--error'); - groupCollection = groupCollection.add(jElem.closest('.govuk-form-group.govuk-form-group--error')); - jElem.removeClass('govuk-input--error'); - jElem.removeClass('govuk-select--error'); - errorCollection.addClass('govuk-visually-hidden'); - groupCollection.removeClass('govuk-form-group--error'); - }; - - this.testError = function (errFn, jElem, errorType) { - let result = false; - - if (errFn(jElem)) { - this.toggleError(jElem, true, errorType); - result = false; - } else { - this.toggleError(jElem, false, errorType); - result = true; - } - - return result; - }; - - this.errorMessage = function (propertyName, errorType) { - switch (errorType) { - case 'required': - return `${propertyName} is required`; - case 'maxlength': - return `${propertyName} is too long`; - case 'min': - return `${propertyName} is too low`; - case 'max': - return `${propertyName} is too high`; - case 'range': - return `${propertyName} is too low or too high`; - case 'number': - return `${propertyName} is not a number`; - case 'email': - return `${propertyName} is not a valid email address`; - case 'pattern': - return `${propertyName} is not valid`; - default: - return `${propertyName} is not valid for the type`; - } - }; - - this.insertElementToCreateFieldBlock = function (inputElement) { - let continueLooping = true; - let counter = 0; - const elementsToWrap = []; - let prevElem = inputElement.prev(); - if (inputElement.next().length > 0 && inputElement.next().prop('class').indexOf('character-count') >= 0) { - elementsToWrap.push(inputElement.next()[0]); - } - elementsToWrap.push(inputElement[0]); - while (continueLooping && counter < 3) { - if (prevElem.length === 0) break; - - if (prevElem[0].nodeName === 'LABEL') { - elementsToWrap.push(prevElem[0]); - } else if (prevElem.prop('class').indexOf('govuk-error-message') >= 0) { - elementsToWrap.push(prevElem[0]); - } else if (counter >= 1) { - continueLooping = false; - } - prevElem = prevElem.prev(); - counter++; - } - const newElem = $('
    ').insertBefore(inputElement); - for (let index = 0; index < elementsToWrap.length; index++) { - newElem.prepend(elementsToWrap[index]); - } - - return newElem; - }; - - this.insertElementForRequiredMessage = function (inputElement, parent, errorType) { - const propertyName = this.getPropertyName(parent); - const labelElem = ``; - $(parent).prepend(labelElem); - const newElement = $(parent[0].childNodes[0]); - const parentElement = inputElement.prev(); - if (parentElement.length > 0 && parentElement[0].tagName === 'LABEL') { - newElement.detach().insertBefore(parentElement); - } else { - newElement.detach().insertBefore(inputElement); - } - return newElement; - }; - - this.clearBannerErrorList = function () { - if (this.bannerErrorContainer != null && this.bannerErrorContainer.length > 0) { - const ul = this.bannerErrorContainer.find('ul'); - ul.empty(); - } - }; - - this.insertListElementInBannerError = function (inputElement, error_type, message_text) { - if (this.bannerErrorContainer != null && this.bannerErrorContainer.length > 0) { - const ul = this.bannerErrorContainer.find('ul'); - let display_text = ''; - if (ul.length > 0) { - const href_value = `#${this.getErrorID(inputElement)}`; - const propertyName = this.getPropertyName(inputElement); - if (typeof message_text === 'undefined' || `${message_text}` === '') { - display_text = this.errorMessage(propertyName, error_type); - } else { - display_text = message_text; - } - var link = ul.find('a').filter(function () { - return $(this).attr('data-propertyname') === propertyName && $(this).text() === display_text; - }); - - if (link.length === 0) { - link = ul.find('a').filter(function () { - return $(this).attr('data-errortype') === error_type && $(this).attr('data-propertyname') === propertyName; - }); - } - - if (link.length === 0) { - link = ul.find('a').filter(function () { - return $(this).attr('href') === href_value && $(this).text() === display_text; - }); - } - - if (link.length === 0) { - link = ul.find('a').filter(function () { - return $(this).text() === display_text; - }); - } - - if (link.length <= 0) { - var link = `${display_text}`; - ul.append(`
  • ${link}
  • `); - } - } - } - }; - - this.removeListElementInBannerError = function (inputElement, error_type) { - if (this.bannerErrorContainer != null && this.bannerErrorContainer.length > 0) { - const ul = this.bannerErrorContainer.find('ul'); - if (ul.length > 0) { - const propertyName = this.getPropertyName(inputElement); - const link = ul.find('a').filter(function () { - return $(this).attr('data-propertyname') === propertyName && $(this).attr('data-errortype') === error_type; - }); - - if (link.length > 0) { - link.remove(); - } - } - } - }; - - this.toggleBannerError = function (bShow) { - if (this.bannerErrorContainer != null && this.bannerErrorContainer.length > 0) { - if (bShow) { - $('#error-summary-title').text('There is a problem'); - this.bannerErrorContainer.removeClass('govuk-visually-hidden'); - this.bannerErrorContainer.get(0).removeAttribute('style'); - $('html, body').animate({ - scrollTop: this.bannerErrorContainer.offset().top, - }, 500); - } else { - const ul = this.bannerErrorContainer.find('ul'); - if (ul.length > 0) { - this.bannerErrorContainer.addClass('govuk-visually-hidden'); - } - } - } - }; - - this.findErrorCollection = function (jQueryElement, errorType, propertyname) { - return $(`div.error-collection[property_name="${propertyname}"`); - }; - - this.findPreExistingErrorMessage = function (jQueryElement, errorType, jqueryElementForInputGroup) { - const property_name = this.getPropertyName(jQueryElement); - let jqueryElementForRequiredMessage = jQueryElement.siblings(`label[data-validation='${errorType}']`); - if (jqueryElementForRequiredMessage.length === 0) { - const errorCollectionForPropertyAndType = this.findErrorCollection(jQueryElement, errorType, property_name); - if (errorCollectionForPropertyAndType.length > 0) { - if (errorCollectionForPropertyAndType.hasClass('potenital-error')) { - const errorLabel = errorCollectionForPropertyAndType.find('label').get(0); - jqueryElementForRequiredMessage = errorLabel.cloneNode(); - jqueryElementForRequiredMessage.setAttribute('data-validation', errorType); - const errorMessage = $(errorLabel).find(`span[data-validation='${errorType}']`).get(0).innerText; - const errorMessageElement = document.createElement('span'); - errorMessageElement.innerText = errorMessage; - jqueryElementForRequiredMessage.append(errorMessageElement); - jqueryElementForRequiredMessage = $(jqueryElementForRequiredMessage); - } else { - jqueryElementForRequiredMessage = errorCollectionForPropertyAndType.find(`label[data-validation='${errorType}']`); - } - } - } - - if (jqueryElementForRequiredMessage.length === 0) { - let collection = []; - let parent = jQueryElement.parent(); - while (parent.length > 0 && collection.length === 0) { - collection = parent.find(`.error-collection[property_name='${property_name}']`); - parent = parent.parent(); - } - if (collection.length > 0) { - jqueryElementForRequiredMessage = collection.find(`label[data-validation='${errorType}']`); - } else { - jqueryElementForRequiredMessage = this.insertElementForRequiredMessage(jQueryElement, jqueryElementForInputGroup, errorType); - } - } - - return jqueryElementForRequiredMessage; - }; - - this.toggleError = function (jQueryElement, show, errorType) { - let error_text = ''; - let jqueryElementForInputGroup = jQueryElement.closest('.govuk-form-group-error-placeholder'); - - if (jqueryElementForInputGroup.length === 0) { - jqueryElementForInputGroup = jQueryElement.closest('.govuk-form-group'); - } - if (jqueryElementForInputGroup.length === 0) { - jqueryElementForInputGroup = this.insertElementToCreateFieldBlock(jQueryElement); - } - let jqueryElementForRequiredMessage = this.findPreExistingErrorMessage(jQueryElement, errorType, jqueryElementForInputGroup); - - if (jQueryElement.prop('class').indexOf('govuk-date-input') < 0) { - if (!jQueryElement.siblings().is(jqueryElementForRequiredMessage)) { - jqueryElementForRequiredMessage = jqueryElementForRequiredMessage.clone(); - jqueryElementForRequiredMessage.prop('id', this.getErrorID(jQueryElement)); - if (jQueryElement.prop('type') === 'radio') { - jqueryElementForRequiredMessage.insertBefore(jQueryElement.parent()); - } else if (jQueryElement.prop('class').indexOf('input-type__input') > -1) { - jqueryElementForRequiredMessage.insertBefore(jQueryElement.parent().parent()); - } else { - jqueryElementForRequiredMessage.insertBefore(jQueryElement); - } - } - } - - if (jqueryElementForRequiredMessage.length > 0) { - error_text = jqueryElementForRequiredMessage[0].innerText; - } - - if (!show) { - if (this.validationResult) { - this.removeErrorClass(jqueryElementForInputGroup); - } - this.removeErrorClass(jQueryElement); - jqueryElementForRequiredMessage.addClass('govuk-visually-hidden'); - jqueryElementForRequiredMessage.css('display', 'none'); - } else { - jqueryElementForRequiredMessage.css('display', 'block'); - this.addErrorClass(jQueryElement); - this.addErrorClass(jqueryElementForInputGroup); - jqueryElementForRequiredMessage.removeClass('govuk-visually-hidden'); - this.insertListElementInBannerError(jQueryElement, errorType, error_text); - } - }; - - this.addErrorClass = function (jQueryElement) { - if (jQueryElement[0].tagName === 'INPUT') { - jQueryElement.addClass('govuk-input--error'); - } else if (jQueryElement[0].tagName === 'SELECT') { - jQueryElement.addClass('govuk-select--error'); - } else if (jQueryElement.prop('class').indexOf('govuk-form-group') >= 0) { - jQueryElement.addClass('govuk-form-group--error'); - } - }; - - this.removeErrorClass = function (jQueryElement) { - if (jQueryElement[0].tagName === 'INPUT') { - jQueryElement.removeClass('govuk-input--error'); - } else if (jQueryElement[0].tagName === 'SELECT') { - jQueryElement.removeClass('govuk-select--error'); - } else if (jQueryElement.prop('class').indexOf('govuk-form-group') >= 0) { - jQueryElement.removeClass('govuk-form-group--error'); - } - }; - - this.getErrorID = function (jqueryInputElement) { - return `error_${jqueryInputElement[0].id}`; - }; - this.getPropertyName = function (jqueryInputElement) { - let propertyName = jqueryInputElement.attr('data-propertyname'); - if (typeof propertyName === 'undefined' || propertyName === '') { - let newParent = null; - if ((newParent = jqueryInputElement.closest('[data-propertyname]')).length > 0) { - propertyName = newParent.attr('data-propertyname'); - } else if ((newParent = jqueryInputElement.closest('[property_name]')).length > 0) { - propertyName = newParent.attr('property_name'); - } - } - if (typeof propertyName === 'undefined' || propertyName === '') { - propertyName = jqueryInputElement[0].id; - } - - return propertyName; - }; - - if (this.verify_connection_to_form(formDOMObject, thisisspecial)) { - this.connect_to_form(formDOMObject, typeof validationCallback === 'undefined' ? this.validateForm : validationCallback); - this.initialise(); - } -} - -const anyArbitraryName = {}; - -$(() => { - anyArbitraryName.global_formValidators = []; - const jqForms = $('form'); - if (jqForms.length > 0) { - for (let index = 0; index < jqForms.length; index++) { - anyArbitraryName.global_formValidators[jqForms[index].id] = new FormValidationComponent( - jqForms[index], void 0, false, - ); - anyArbitraryName.global_formValidators.push(anyArbitraryName.global_formValidators[jqForms[index].id]); - } - } -}); diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-buildings-services-lifts.js b/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-buildings-services-lifts.js deleted file mode 100644 index 54a46cd3fa..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-buildings-services-lifts.js +++ /dev/null @@ -1,70 +0,0 @@ -function procurementBuildingServicesLifts() { - const maxLifts = 99; - - function getNumberOfLifts() { - return $('.lift-row').length; - } - - function getRowCount() { - $('.lift-row').each(function (i) { - this.getElementsByClassName('lift-number')[0].textContent = `Lift ${i + 1}`; - }); - } - - function updateAddButton() { - const addButton = $('.add-lift-button')[0]; - const numberRemaining = maxLifts - getNumberOfLifts(); - - addButton.textContent = `Add another lift (${numberRemaining} remaining)`; - } - - function updateRemoveButtons() { - $('.lift-row').each(function (i) { - const removeButton = $(this.getElementsByClassName('remove-lift-record')[0]); - - if (i + 1 < getNumberOfLifts() || getNumberOfLifts() === 1) { - removeButton.attr('tabindex', '-1'); - removeButton.addClass('govuk-visually-hidden'); - } else { - removeButton.removeAttr('tabindex'); - removeButton.removeClass('govuk-visually-hidden'); - } - }); - } - - $('form').on('click', '.remove-lift-record', function (e) { - const liftRow = $(this).closest('div'); - - $(this).next().val('true'); - liftRow.addClass('removed-lift-row'); - liftRow.closest('div').removeClass('lift-row'); - liftRow.hide(); - getRowCount(); - updateRemoveButtons(); - updateAddButton(); - return e.preventDefault(); - }); - - $('form').on('click', '.add-lift-button', function (e) { - if (getNumberOfLifts() < maxLifts) { - const time = new Date().getTime(); - const regexp = new RegExp($(this).data('id'), 'g'); - $('.fields').append($(this).data('fields').replace(regexp, time)); - getRowCount(); - updateRemoveButtons(); - updateAddButton(); - return e.preventDefault(); - } - return e.preventDefault(); - }); - - getRowCount(); - updateRemoveButtons(); - updateAddButton(); -} - -$(() => { - if ($('.liftdatacontainer').length) { - procurementBuildingServicesLifts(); - } -}); diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-pension-funds.js b/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-pension-funds.js deleted file mode 100644 index 21a465ec9e..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/procurement-pension-funds.js +++ /dev/null @@ -1,64 +0,0 @@ -function pensionFund() { - const maxPensionFunds = 99; - - function getNumberOfPensions() { - return $('.pension-row').length; - } - - function checkIfOneRow() { - if (getNumberOfPensions() === 1) { - $('.remove-pension-record').addClass('govuk-visually-hidden'); - $($('.pension-row').get(0)).find('.remove-pension-record').get(0).setAttribute('tabindex', -1); - } else if (getNumberOfPensions() > 1) { - $('.remove-pension-record').removeClass('govuk-visually-hidden'); - $($('.pension-row').get(0)).find('.remove-pension-record').get(0).removeAttribute('tabindex'); - } - } - - function getRowCount() { - $('.pension-row').each(function (i) { - this.getElementsByClassName('pension-number')[0].textContent = `Pension fund name ${i + 1}`; - }); - } - - function pensionToAddLeft() { - const pensionsLeft = maxPensionFunds - getNumberOfPensions(); - if (document.getElementsByClassName('add-pension-button').length) { - $('.add-pension-button')[0].textContent = `Add another pension fund (${pensionsLeft} remaining)`; - } - } - - $('form').on('click', '.remove-pension-record', function (e) { - const pensionRow = $(this).closest('div').parent().closest('div'); - - $(this).next().val('true'); - pensionRow.addClass('removed-pension-row'); - pensionRow.closest('div').removeClass('pension-row'); - pensionRow.hide(); - checkIfOneRow(); - getRowCount(); - pensionToAddLeft(); - return e.preventDefault(); - }); - - $('form').on('click', '.add-pension-fields', function (e) { - if (getNumberOfPensions() < maxPensionFunds) { - const time = new Date().getTime(); - const regexp = new RegExp($(this).data('id'), 'g'); - $('.fields').append($(this).data('fields').replace(regexp, time)); - checkIfOneRow(); - getRowCount(); - pensionToAddLeft(); - return e.preventDefault(); - } - return e.preventDefault(); - }); - - checkIfOneRow(); - getRowCount(); - pensionToAddLeft(); -} - -$(() => { - pensionFund(); -}); diff --git a/app/assets/javascripts/facilities-management/rm3830/procurement/quick-search-results.js b/app/assets/javascripts/facilities-management/rm3830/procurement/quick-search-results.js deleted file mode 100644 index 0dd2ddb440..0000000000 --- a/app/assets/javascripts/facilities-management/rm3830/procurement/quick-search-results.js +++ /dev/null @@ -1,23 +0,0 @@ -$(() => { - $('#results-filter-button').on('click', (e) => { - e.preventDefault(); - - const requirementsPlane = $('#requirements-list'); - const suppliersPlane = $('#supplier-lot-list__container'); - - const btn = $('#results-filter-button'); - const isHidden = requirementsPlane.is(':hidden'); - const curText = btn.text(); - - if (isHidden) { - requirementsPlane.show(); - suppliersPlane.addClass('govuk-grid-column-two-thirds').removeClass('govuk-grid-column-full'); - } else { - requirementsPlane.hide(); - suppliersPlane.addClass('govuk-grid-column-full').removeClass('govuk-grid-column-two-thirds'); - } - - btn.text(btn.attr('alt-text')); - btn.attr('alt-text', curText); - }); -}); diff --git a/app/assets/javascripts/facilities-management/rm6232/admin/supplier-data-snapshot.js b/app/assets/javascripts/facilities-management/rm6232/admin/supplier-data-snapshot.js deleted file mode 100644 index e149b2f67c..0000000000 --- a/app/assets/javascripts/facilities-management/rm6232/admin/supplier-data-snapshot.js +++ /dev/null @@ -1,17 +0,0 @@ -$(() => { - const enableZipButton = () => $('#generate-supplier-zip-button').attr('disabled', false); - - const disableZipButtonAndHideErrors = () => { - $('.govuk-error-message').remove(); - $('.govuk-form-group').removeClass('govuk-form-group--error'); - $('#generate-supplier-zip-button').attr('disabled', true); - }; - - $('#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_date_dd').on('input', enableZipButton); - $('#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_date_mm').on('input', enableZipButton); - $('#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_date_yyyy').on('input', enableZipButton); - $('#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_time_hh').on('input', enableZipButton); - $('#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_time_mm').on('input', enableZipButton); - - $('#generate-supplier-zip').on('submit', disableZipButtonAndHideErrors); -}); diff --git a/app/assets/javascripts/global/global.js b/app/assets/javascripts/global/global.js deleted file mode 100644 index 6b0c738c80..0000000000 --- a/app/assets/javascripts/global/global.js +++ /dev/null @@ -1,171 +0,0 @@ -function ifChecked(sWrap, h) { - let howmany; - - if (sWrap === false) { - howmany = 0; - } else { - howmany = sWrap.find('.govuk-checkboxes__input:checked').length; - } - - if (howmany > 0) { - const txt = h.data('txt'); - h.find('.ccs-filter-no').text(howmany).end().find('.ccs-filter-txt') - .text(txt) - .end() - .addClass('ccs-hint--show'); - } else { - h.removeClass('ccs-hint--show').find('.ccs-filter-no').empty().end() - .find('.ccs-filter-txt') - .empty(); - } - } - - function whenChecked(sWrap, h) { - const allcheckbxs = sWrap.find('.govuk-checkboxes__input'); - - allcheckbxs.on('change', () => { - ifChecked(sWrap, h); - }); - } - - function initSearchResults(id) { - const accord = id.find('.ccs-at-checkbox-accordian'); - - accord.each(function (index) { - const shopWrap = $(this); - - const hint = shopWrap.find('.ccs-govuk-hint--selected'); - if (hint.length) { - ifChecked(shopWrap, hint); - whenChecked(shopWrap, hint); - } - - const link = $(this).find('.ccs-at-btn-toggle'); - - link.attr('aria-expanded', 'false').on('click', function (e) { - e.preventDefault(); - - $(this).attr('aria-expanded', $(this).attr('aria-expanded') === 'true' ? 'false' : 'true') - .find('span').text((i, text) => (text === 'Hide' ? ' Show' : ' Hide')); - - if (shopWrap.hasClass('show')) { - shopWrap.removeClass('show'); - } else { - shopWrap.addClass('show'); - } - }); - }); - - const checkboxs = accord.find('.govuk-checkboxes__input'); - - checkboxs.keypress(function (e) { - if ((e.keyCode ? e.keyCode : e.which) == 13) { - $(this).trigger('click'); - } - }); - - $('#ccs-clear-filters').on('click', (e) => { - e.preventDefault(); - checkboxs.prop('checked', false); - - const hint = id.find('.ccs-govuk-hint--selected'); - if (hint.length) { - ifChecked(false, hint); - } - }); - } - - function updateTitle(i, v, b) { - const span = b.find('span:first-child'); - - if (v === true) { - span.text(i); - $('#removeAll').removeClass('ccs-remove'); - headerTxt(b, true); - } else { - span.empty(); - $('#removeAll').addClass('ccs-remove'); - headerTxt(b, false); - } - } - - function headerTxt(header, t) { - let tx; - if (t === true) { - tx = header.data('txt01'); - } else { - tx = header.data('txt02'); - } - header.find('span:last-child').text(tx); - } - - function updateList(govb, id, basket) { - let i = ''; - let thelist = ''; - let $this; - const list = id.find('ul'); - const thecheckboxes = govb.find('.govuk-checkboxes__item').not('.ccs-select-all').find('.govuk-checkboxes__input:checked'); - - list.find('.ccs-removethis').remove(); - - if (thecheckboxes.length) { - thecheckboxes.each(function (index) { - $this = $(this); - thelist = `${thelist}
  • ${$this.next('label').text()} Remove
  • `; - i = index + 1; - }); - updateTitle(i, true, basket); - } else { - updateTitle(i, false, basket); - } - - list.append(thelist).find('a').on('click', function (e) { - e.preventDefault(); - const thisbox = $(`#${$(this).data('id')}`); - - $(this).parent().remove(); - thisbox.prop('checked', false); - i -= 1; - if (i > 0) { - updateTitle(i, true, basket); - } else { - updateTitle(i, false, basket); - } - - const theparent = thisbox.parents('.govuk-checkboxes').find('.ccs-select-all').find('.govuk-checkboxes__input:checked'); - if (theparent.length) { - theparent.prop('checked', false); - } - }); - - $('#removeAll').on('click', function (e) { - e.preventDefault(); - list.find('.ccs-removethis').remove(); - govb.find('.govuk-checkboxes__input:checked').prop('checked', false); - headerTxt(basket, false); - $(this).addClass('ccs-remove').siblings().find('span:first-child') - .empty(); - }); - } - - function initStepByStepNav() { - const $element = $('#step-by-step-navigation'); - const stepByStepNavigation = new window.GOVUKFrontend.Modules.Gemstepnav($element.get(0)) - stepByStepNavigation.init(); - } - - function initCustomFnc() { - const filt = $('#ccs-at-results-filters'); - if (filt.length) { - initSearchResults(filt); - } - - if ($('#step-by-step-navigation').length) { - initStepByStepNav(); - } - } - -$(() => { - initCustomFnc(); - }); - \ No newline at end of file diff --git a/app/assets/javascripts/google-analytics.js b/app/assets/javascripts/google-analytics.js deleted file mode 100644 index 5617c720b0..0000000000 --- a/app/assets/javascripts/google-analytics.js +++ /dev/null @@ -1,33 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - var cssClassToClickPath = { - 'ga-crown-logo': 'crown_logo', - 'ga-feedback-mailto': 'feedback', - 'ga-support-mailto': 'support', - 'ga-auth-cognito': 'cognito_login', - 'ga-auth-dfe': 'dfe_login', - 'ga-print-link': 'print', - 'ga-download-shortlist': 'shortlist_download', - 'ga-download-calculator': 'calculator_download' - }; - - if (window.gaTrackingId) { - for (var cssClass in cssClassToClickPath) { - var elements = document.getElementsByClassName(cssClass); - for (var idx=0; idx < elements.length; idx++) { - var element = elements[idx]; - var onClick = function(cssClass) { - return function() { - var page = cssClassToClickPath[cssClass]; - var params = { - 'anonymize_ip': true, - 'page_title': page, - 'page_path': '/external/'+page - }; - gtag('config', window.gaTrackingId, params); - } - }(cssClass); - element.addEventListener('click', onClick, false); - } - } - } -}); diff --git a/app/controllers/facilities_management/admin/management_reports_controller.rb b/app/controllers/facilities_management/admin/management_reports_controller.rb index 5bc9619e8d..97ba16446b 100644 --- a/app/controllers/facilities_management/admin/management_reports_controller.rb +++ b/app/controllers/facilities_management/admin/management_reports_controller.rb @@ -28,7 +28,7 @@ def create end def progress - render json: { status: @management_report.aasm_state } + render json: { import_status: @management_report.aasm_state } end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ac100039c2..243782cdda 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -262,13 +262,8 @@ def warning_text(text) end end - def create_find_address_helper(object, organisaiton_prefix, object_name, postcode_name) - @find_address_helper = FacilitiesManagement::FindAddressHelper.new(object, organisaiton_prefix) - - capture do - concat(hidden_field_tag(:object_name, object_name)) - concat(hidden_field_tag(:postcode_name, postcode_name)) - end + def find_address_helper(object, organisaiton_prefix) + FacilitiesManagement::FindAddressHelper.new(object, organisaiton_prefix) end def hidden_class(visible) @@ -382,6 +377,17 @@ def can_show_new_framework_banner? Marketplace.rm6232_live? || params[:show_new_framework_banner].present? end + # rubocop:disable Metrics/ParameterLists + def link_to_add_row(name, number_of_items, form, association, partial_prefix, **args) + new_object = form.object.send(association).klass.new + id = new_object.object_id + fields = form.fields_for(association, new_object, child_index: id) do |builder| + render("#{partial_prefix}/#{association.to_s.singularize}", ff: builder) + end + link_to(name.gsub('', number_of_items.to_s), '#', class: args[:class], data: { id: id, fields: fields.gsub('\n', ''), 'button-text': name }) + end + # rubocop:enable Metrics/ParameterLists + def cookie_preferences_settings @cookie_preferences_settings ||= begin current_cookie_preferences = JSON.parse(cookies[Marketplace.cookie_settings_name] || '{}') diff --git a/app/helpers/facilities_management/buildings_helper.rb b/app/helpers/facilities_management/buildings_helper.rb index 9130682f2d..c6bd9d907a 100644 --- a/app/helpers/facilities_management/buildings_helper.rb +++ b/app/helpers/facilities_management/buildings_helper.rb @@ -101,4 +101,8 @@ def full_region_visible? def multiple_regions? valid_regions.length > 1 end + + def object_name(name) + name + end end diff --git a/app/helpers/facilities_management/find_address_helper.rb b/app/helpers/facilities_management/find_address_helper.rb index 74b7f31bed..ef2e3d12a6 100644 --- a/app/helpers/facilities_management/find_address_helper.rb +++ b/app/helpers/facilities_management/find_address_helper.rb @@ -2,32 +2,31 @@ module FacilitiesManagement class FindAddressHelper include FacilitiesManagement::FindAddressConcern - attr_accessor :object, :address_postcode, :address_postcode_symbol, :address_line1, :address_line2, :address_town, :address_county + attr_accessor :object, :address_keys, :address_postcode, :address_line1, :address_line2, :address_town, :address_county def initialize(object, organisaiton_prefix) @object = object - if organisaiton_prefix - @address_postcode = object.organisation_address_postcode - @address_postcode_symbol = :organisation_address_postcode - @address_line1 = object.organisation_address_line_1 - @address_line2 = object.organisation_address_line_2 - @address_town = object.organisation_address_town - @address_county = object.organisation_address_county - else - @address_postcode = object.address_postcode - @address_postcode_symbol = :address_postcode - @address_line1 = object.address_line_1 - @address_line2 = @object.address_line_2 - @address_town = object.address_town - end + @address_keys = if organisaiton_prefix + @county_field = true + ATTRIBUTES_TO_KEY[:organisaiton_prefix_true] + else + @county_field = false + ATTRIBUTES_TO_KEY[:organisaiton_prefix_false] + end + + @address_postcode = object[@address_keys[:address_postcode]] + @address_line1 = object[@address_keys[:address_line1]] + @address_line2 = object[@address_keys[:address_line2]] + @address_town = object[@address_keys[:address_town]] + @address_county = object[@address_keys[:address_county]] end def postcode_search_visible? - @postcode_search_visible ||= address_postcode.blank? || object.errors[address_postcode_symbol].any? + @postcode_search_visible ||= address_postcode.blank? || object.errors[@address_keys[:address_postcode]].any? end def postcode_change_visible? - @postcode_change_visible ||= address_postcode.present? && object.errors[address_postcode_symbol].none? && address_line1.blank? + @postcode_change_visible ||= address_postcode.present? && object.errors[@address_keys[:address_postcode]].none? && address_line1.blank? end def select_an_address_visible? @@ -49,5 +48,21 @@ def address_count def address_in_a_line [address_line1, address_line2, address_town, address_county].reject(&:blank?).join(', ') + " #{address_postcode}" end + + ATTRIBUTES_TO_KEY = { + organisaiton_prefix_true: { + address_postcode: :organisation_address_postcode, + address_line1: :organisation_address_line_1, + address_line2: :organisation_address_line_2, + address_town: :organisation_address_town, + address_county: :organisation_address_county + }, + organisaiton_prefix_false: { + address_postcode: :address_postcode, + address_line1: :address_line_1, + address_line2: :address_line_2, + address_town: :address_town + } + }.freeze end end diff --git a/app/helpers/facilities_management/rm3830/procurement_buildings_services_helper.rb b/app/helpers/facilities_management/rm3830/procurement_buildings_services_helper.rb index 72c13c5998..c1c204f025 100644 --- a/app/helpers/facilities_management/rm3830/procurement_buildings_services_helper.rb +++ b/app/helpers/facilities_management/rm3830/procurement_buildings_services_helper.rb @@ -14,15 +14,6 @@ def sort_by_lifts_created_at parts.last.sort_by(&:created_at) + parts.first end - def link_to_lift_add_row(name, form, association, **args) - new_object = form.object.send(association).klass.new - id = new_object.object_id - fields = form.fields_for(association, new_object, child_index: id) do |builder| - render("facilities_management/rm3830/procurement_buildings_services/#{association.to_s.singularize}", ff: builder) - end - link_to(name, '#', class: "add-lifts #{args[:class]}", data: { id: id, fields: fields.gsub('\n', '') }) - end - def form_model params[:service_question] == 'area' ? @building : @building_service end diff --git a/app/helpers/facilities_management/rm3830/procurements/contract_details_helper.rb b/app/helpers/facilities_management/rm3830/procurements/contract_details_helper.rb index 093b97b7d3..916efdfe49 100644 --- a/app/helpers/facilities_management/rm3830/procurements/contract_details_helper.rb +++ b/app/helpers/facilities_management/rm3830/procurements/contract_details_helper.rb @@ -27,15 +27,6 @@ def supplier_plural 'supplier'.pluralize(sorted_supplier_list.count) end - def link_to_add_row(name, form, association, **args) - new_object = form.object.send(association).klass.new - id = new_object.object_id - fields = form.fields_for(association, new_object, child_index: id) do |builder| - render("#{partial_prefix}/#{association.to_s.singularize}", ff: builder) - end - link_to(name, '#', class: "add-pension-fields #{args[:class]}", data: { id: id, fields: fields.gsub('\n', '') }) - end - def object_name(name) name.gsub('[', '_')[0..-2] end diff --git a/app/helpers/gov_uk_helper.rb b/app/helpers/gov_uk_helper.rb index 89a4e2a49a..ba7d9c4e90 100644 --- a/app/helpers/gov_uk_helper.rb +++ b/app/helpers/gov_uk_helper.rb @@ -8,6 +8,7 @@ module GovUKHelper include NotificationBanner include Pagination include Radios + include Select include StepByStepNavigation include TextInput end diff --git a/app/helpers/gov_uk_helper/select.rb b/app/helpers/gov_uk_helper/select.rb new file mode 100644 index 0000000000..f240ea81bb --- /dev/null +++ b/app/helpers/gov_uk_helper/select.rb @@ -0,0 +1,30 @@ +module GovUKHelper::Select + def govuk_select(form, attribute, select_options) + class_list = ['govuk-form-group'] + any_errors = form.object.errors.include? attribute + class_list << 'govuk-form-group--error' if any_errors + + tag.div(class: class_list, id: "#{attribute}-form-group") do + capture do + concat(govuk_label(form, attribute, select_options[:label])) if select_options[:label] + concat(govuk_hint(select_options[:hint])) if select_options[:hint] + concat(govuk_error_message(form, attribute)) if any_errors + concat(govuk_select_input(attribute, any_errors, select_options[:select])) + end + end + end + + private + + def govuk_select_input(attribute, any_errors, select_options) + class_list = ['govuk-select'] + class_list << select_options.delete(:classes) if select_options[:classes] + class_list << 'govuk-select--error' if any_errors + + options = select_options[:options].map do |select_option| + [select_option[:text] || select_option[:value], select_option[:value], select_option[:attributes]] + end + + select_tag(attribute, options_for_select(options, select_options[:selected]), class: class_list, prompt: select_options[:prompt], **(select_options[:attributes] || {})) + end +end diff --git a/app/helpers/gov_uk_helper/text_input.rb b/app/helpers/gov_uk_helper/text_input.rb index 7cc5ca442b..a1de0e42f8 100644 --- a/app/helpers/gov_uk_helper/text_input.rb +++ b/app/helpers/gov_uk_helper/text_input.rb @@ -42,7 +42,7 @@ def govuk_input(form, attribute, any_errors, input_options = {}) prefix = input_options.delete(:prefix) suffix = input_options.delete(:suffix) - text_field = form.text_field(attribute, class: class_list, **input_options) + text_field = form.text_field(attribute, class: class_list, **input_options[:attributes]) if prefix || suffix tag.div(class: 'govuk-input__wrapper') do diff --git a/app/typescript/.eslintrc.json b/app/typescript/.eslintrc.json new file mode 100644 index 0000000000..dc7757c235 --- /dev/null +++ b/app/typescript/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "overrides": [ + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "never" + ] + } +} diff --git a/app/typescript/facilitiesManagement/buildings/buildingType.ts b/app/typescript/facilitiesManagement/buildings/buildingType.ts new file mode 100644 index 0000000000..e217f8ec39 --- /dev/null +++ b/app/typescript/facilitiesManagement/buildings/buildingType.ts @@ -0,0 +1,26 @@ +const enableRadios = (detailsOpen: boolean) => { + const $radioInputs: JQuery = $('.govuk-details__text .govuk-radios__item') + + if (detailsOpen === true) { + $radioInputs.each((_, element: HTMLElement) => { + $(element).find('input').removeAttr('disabled') + }) + } else { + $radioInputs.each((_, element: HTMLElement) => { + $(element).find('input').attr('disabled', 'disabled') + }) + } +} + +const initBuildingType = (): void => { + if ($('.building-type').length) { + const $govukDetails: JQuery = $('.govuk-details') + + $govukDetails.on('toggle', (event: JQuery.TriggeredEvent) => { + enableRadios($(event.currentTarget).attr('open') === 'open') + }) + } +} + + +$(() => initBuildingType()) diff --git a/app/typescript/facilitiesManagement/buildings/detailsInStorage.ts b/app/typescript/facilitiesManagement/buildings/detailsInStorage.ts new file mode 100644 index 0000000000..12275cb4a8 --- /dev/null +++ b/app/typescript/facilitiesManagement/buildings/detailsInStorage.ts @@ -0,0 +1,45 @@ +type BuildingDetailsInputElements = { + $buildingName: JQuery + $buildingDescription: JQuery +} + +const getBuildingDetails = (buildingDetailsInputElements: BuildingDetailsInputElements): void => { + window.sessionStorage.buildingDetailsBuildingName = buildingDetailsInputElements.$buildingName.val() + window.sessionStorage.buildingDetailsBuildingDescription = buildingDetailsInputElements.$buildingDescription.val() + window.sessionStorage.buildingDetails = true +} + +const fillInBuildingDetails = (buildingDetailsInputElements: BuildingDetailsInputElements): void => { + buildingDetailsInputElements.$buildingName.val(window.sessionStorage.buildingDetailsBuildingName) + buildingDetailsInputElements.$buildingDescription.val(window.sessionStorage.buildingDetailsBuildingDescription) +} + +const removeBuildingDetails = (): void => { + window.sessionStorage.removeItem('buildingDetails') + window.sessionStorage.removeItem('buildingDetailsBuildingName') + window.sessionStorage.removeItem('buildingDetailsBuildingDescription') +} + +const initBuildingsInStorage = (): void => { + if (typeof (Storage) !== 'undefined') { + const buildingDetailsInputElements: BuildingDetailsInputElements = { + $buildingName: $('#facilities_management_building_building_name'), + $buildingDescription: $('#facilities_management_building_description') + } + + const $formBuildingDetails: JQuery = $('#building-details-form') + const $formBuildingAddressDetails: JQuery = $('#building-address-details-form') + + if ($formBuildingDetails.length && window.sessionStorage.buildingDetails) fillInBuildingDetails(buildingDetailsInputElements) + + if ($formBuildingDetails.length) { + $('#cant-find-address-link').on('click', () => getBuildingDetails(buildingDetailsInputElements)) + + $('input[type="submit"]').on('click', () => removeDetails()) + } else if (!$formBuildingAddressDetails.length) { + removeBuildingDetails() + } + } +} + +$(() => initBuildingsInStorage()) diff --git a/app/typescript/facilitiesManagement/detailsInStorage.ts b/app/typescript/facilitiesManagement/detailsInStorage.ts new file mode 100644 index 0000000000..31890a9605 --- /dev/null +++ b/app/typescript/facilitiesManagement/detailsInStorage.ts @@ -0,0 +1,83 @@ + +type ContactDetailsInputElements = { + $name: JQuery + $fullName: JQuery + $jobTitle: JQuery + $email: JQuery + $telephoneNumber: JQuery + $organisationName: JQuery + $sectorTrue: JQuery + $sectorFalse: JQuery +} + +const makeElements = (): ContactDetailsInputElements => { + const modelName: string = $('[data-module="find-address"]').data('modelName') + + return { + $name: $(`#${modelName}_name`), + $fullName: $(`#${modelName}_full_name`), + $jobTitle: $(`#${modelName}_job_title`), + $email: $(`#${modelName}_email`), + $telephoneNumber: $(`#${modelName}_telephone_number`), + $organisationName: $(`#${modelName}_organisation_name`), + $sectorTrue: $(`#${modelName}_central_government_true`), + $sectorFalse: $(`#${modelName}_central_government_false`), + } +} + +const getDetails = (contactDetailsInputElements: ContactDetailsInputElements): void => { + window.sessionStorage.contactDetailsName = contactDetailsInputElements.$name.val() + window.sessionStorage.contactDetailsfullName = contactDetailsInputElements.$fullName.val() + window.sessionStorage.contactDetailsJobTitle = contactDetailsInputElements.$jobTitle.val() + window.sessionStorage.contactDetailsEmail = contactDetailsInputElements.$email.val() + window.sessionStorage.contactDetailsTelephoneNumber = contactDetailsInputElements.$telephoneNumber.val() + window.sessionStorage.contactDetailsOrgName = contactDetailsInputElements.$organisationName.val() + window.sessionStorage.contactDetailsSectorTrue = contactDetailsInputElements.$sectorTrue.is(':checked') + window.sessionStorage.contactDetailsSectorFalse = contactDetailsInputElements.$sectorFalse.is(':checked') + + window.sessionStorage.contactDetails = true +} + +const fillInDetails = (contactDetailsInputElements: ContactDetailsInputElements): void => { + contactDetailsInputElements.$name.val(window.sessionStorage.contactDetailsName) + contactDetailsInputElements.$fullName.val(window.sessionStorage.contactDetailsfullName) + contactDetailsInputElements.$jobTitle.val(window.sessionStorage.contactDetailsJobTitle) + contactDetailsInputElements.$email.val(window.sessionStorage.contactDetailsEmail) + contactDetailsInputElements.$telephoneNumber.val(window.sessionStorage.contactDetailsTelephoneNumber) + contactDetailsInputElements.$organisationName.val(window.sessionStorage.contactDetailsOrgName) + contactDetailsInputElements.$sectorTrue.prop('checked', window.sessionStorage.contactDetailsSectorTrue === 'true') + contactDetailsInputElements.$sectorFalse.prop('checked', window.sessionStorage.contactDetailsSectorFalse === 'true') +} + +const removeDetails = () => { + window.sessionStorage.removeItem('contactDetails') + window.sessionStorage.removeItem('contactDetailsName') + window.sessionStorage.removeItem('contactDetailsfullName') + window.sessionStorage.removeItem('contactDetailsJobTitle') + window.sessionStorage.removeItem('contactDetailsEmail') + window.sessionStorage.removeItem('contactDetailsTelephoneNumber') + window.sessionStorage.removeItem('contactDetailsOrgName') + window.sessionStorage.removeItem('contactDetailsSectorTrue') + window.sessionStorage.removeItem('contactDetailsSectorFalse') +} + +const initContactDetailsInStorage = (): void => { + if (typeof (Storage) !== 'undefined') { + const contactDetailsInputElements: ContactDetailsInputElements = makeElements() + + const $formContactDetails: JQuery = $('#edit-contact-detail') + const $formContactDetailsAddress: JQuery = $('#edit-contact-detail-address') + + if ($formContactDetails.length && window.sessionStorage.contactDetails) fillInDetails(contactDetailsInputElements) + + if ($formContactDetails.length) { + $('#cant-find-address-link').on('click', () => getDetails(contactDetailsInputElements)) + + $('input[type="submit"]').on('click', () => removeDetails()) + } else if (!$formContactDetailsAddress.length) { + removeDetails() + } + } +} + +$(() => initContactDetailsInStorage()) diff --git a/app/typescript/facilitiesManagement/detailsLinks.ts b/app/typescript/facilitiesManagement/detailsLinks.ts new file mode 100644 index 0000000000..3d399c1f7b --- /dev/null +++ b/app/typescript/facilitiesManagement/detailsLinks.ts @@ -0,0 +1,25 @@ +const toggleLinksInDetails = ($details: JQuery) => { + const $detailLinks: JQuery = $details.find('a') + + if ($details.attr('open') === 'open') { + $detailLinks.each((_, element: HTMLElement) => { + $(element).removeAttr('tabindex') + }) + } else { + $detailLinks.each((_, element: HTMLElement) => { + $(element).attr('tabindex', -1) + }) + } +} + +const initDetailsLinks = (): void => { + const $govukDetails: JQuery = $('.govuk-details') + + if ($govukDetails.length) { + $govukDetails.on('toggle', (event: JQuery.TriggeredEvent) => { + toggleLinksInDetails($(event.currentTarget)) + }) + } +} + +$(() => initDetailsLinks()) diff --git a/app/typescript/facilitiesManagement/filterTable.ts b/app/typescript/facilitiesManagement/filterTable.ts new file mode 100644 index 0000000000..8e1d4f8582 --- /dev/null +++ b/app/typescript/facilitiesManagement/filterTable.ts @@ -0,0 +1,46 @@ +class FilterTableRow { + private $row: JQuery + private rowValue: string + + constructor($row: JQuery, column: number) { + this.$row = $row + this.rowValue = (this.$row.children().get(column)?.textContent || '').toUpperCase() + } + + checkRowAndToggleVisibility = (filter: string) => { + const isShown: boolean = this.rowValue.indexOf(filter) > -1 + + if (isShown) { + this.$row.show() + } else { + this.$row.hide() + } + } +} + +class FilterTable { + private $searchBox: JQuery + private column: number + private tableRows: FilterTableRow[] = [] + + constructor() { + this.$searchBox = $('#fm-table-filter-input') + this.column = Number($('#fm-table-filter-input').attr('data-column')) || 0 + + const $tableRows: JQuery = $('#fm-table-filter').find('tbody').find('tr') + + $tableRows.each((_index: number, tableRow: HTMLTableRowElement) => { + this.tableRows.push(new FilterTableRow($(tableRow), this.column)) + }) + + this.setEventListners() + } + + private setEventListners = () => this.$searchBox.on('keyup', () => this.filterTable(String(this.$searchBox.val() || '').toUpperCase())) + + private filterTable = (filter: string): void => this.tableRows.forEach(tableRow => tableRow.checkRowAndToggleVisibility(filter)) +} + +$(() => { + if ($('#fm-table-filter-input').length > 0) new FilterTable() +}) diff --git a/app/typescript/facilitiesManagement/findAddress.ts b/app/typescript/facilitiesManagement/findAddress.ts new file mode 100644 index 0000000000..4dc777c024 --- /dev/null +++ b/app/typescript/facilitiesManagement/findAddress.ts @@ -0,0 +1,668 @@ + +type PostcodeResult = { + valid: false + input: string + fullPostcode?: never +} | { + valid: true + input: string + fullPostcode: string +} + +type ErrorMessages = { + invalid: string +} + +type AddressResult = { + summary_line: string + address_line_1: string + address_line_2: string + address_town: string + address_postcode: string +} + +type RegionResult = { + code: string + region: string +} + +type ResultTextPromptOptions = { + textSingle: string + textPlural: string +} + +type AddressElements = { + summaryLine: string + addressLine1: string + addressLine2: string + addressTown: string + addressPostcode: string +} + +type RegionElements = { + addressRegion: string + addressRegionCode: string +} + +type HiddenAddressInputs = { + addressLine1: JQuery + addressLine2: JQuery + addressTown: JQuery + addressCounty: JQuery +} + +type HiddenRegionInputs = { + addressRegion: JQuery + addressRegionCode: JQuery +} + +interface FindAddressSelectSectionInterface { + processResult: (results: AddressResult[] | RegionResult[]) => void + clearSelection: () => void +} + +interface FindAddressTextSectionInterface { + clearSelectedItems: () => void +} + +interface PostcodeSearchInterface { + getPostcode: () => string +} + +interface PostcodeChangeInterface { + setPostcode: (postcode: string) => void +} + +interface SelectAnAddressInterface { + getAddressElements: () => AddressElements +} + +interface SelectedAddressInterface { + populateSelectedAddress: (addressElements: AddressElements) => void +} + +interface SelectARegionInterface { + getRegionElements: () => RegionElements + isOneResult: () => boolean + selectOnlyRegion: () => void + resetSelectedRegion: () => void +} + +interface SelectedRegionInterface { + populateSelectedRegion: (regionElements: RegionElements) => void + toggleChangeLinkVisibility: (isShown: boolean) => void +} + +interface FindAddressInterface { + findAddressFromPostcode: () => void + changePostcode: () => void + addressesFound: () => void + addressSelected: () => void + changeAddress: () => void + regionsFound: () => void + regionSelected: () => void + changeRegion: () => void +} + +const callAPI = (module: FindAddressSelectSection, url: string): void => { + let result: AddressResult[] | RegionResult[] + + $.ajax({ + type: 'GET', + url: encodeURI(url), + dataType: 'json', + success(data) { + result = data.result + }, + error() { + result = [] + }, + complete() { + module.processResult(result) + } + }) +} + +abstract class FindAddressSection { + protected findAddress + protected $section: JQuery + + constructor (findAddress: FindAddress, $section: JQuery) { + this.findAddress = findAddress + this.$section = $section + } + + toggleSectionVisibility = (isShown: boolean) => { + let tabindex + + if (isShown) { + this.$section.removeClass('govuk-visually-hidden') + tabindex = 0 + } else { + this.$section.addClass('govuk-visually-hidden') + tabindex = -1 + } + + if (this.$section.attr('tabindex')) this.$section.attr('tabIndex', tabindex) + this.$section.find('[tabindex]').attr('tabIndex', tabindex) + } + + abstract focus(): JQuery + protected _focus = ($element: JQuery): JQuery => $element.trigger('focus') +} + +abstract class FindAddressInputSection extends FindAddressSection { + protected $input: JQuery + protected inputErrorClass: string + protected $errorMessage?: JQuery + protected errorMessageID = `${this.$section.data('propertyname') || ''}-error` + protected $formGroup: JQuery + protected errorMessageText?: string + + constructor(findAddress: FindAddress, $section: JQuery, $input: JQuery, inputType: string) { + super(findAddress, $section) + + this.$input = $input + this.inputErrorClass = `govuk-${inputType}--error` + this.$formGroup = $input.parent('.govuk-form-group') + if ($input.data('error-messages') !== undefined) this.errorMessageText = ($input.data('error-messages') as ErrorMessages).invalid + + this.setErrorMessageElement() + } + + private setErrorMessageElement = (): void => { + this.$errorMessage = $(`#${this.errorMessageID}`) + } + + protected showError = (): void => { + const errorMessageHTML = `${this.errorMessageText}` + + this.$input.before(errorMessageHTML) + this.setErrorMessageElement() + + this.$formGroup.addClass('govuk-form-group--error') + this.$input.addClass(this.inputErrorClass) + } + + protected clearError = (): void => { + if (this.$errorMessage !== undefined) { + this.$errorMessage.remove() + delete this.$errorMessage + } + + this.$formGroup.removeClass('govuk-form-group--error') + this.$input.removeClass(this.inputErrorClass) + } +} + +abstract class FindAddressSelectSection extends FindAddressInputSection implements FindAddressSelectSectionInterface { + protected resultTextPromptOptions: ResultTextPromptOptions = this.$input.data() as ResultTextPromptOptions + protected $selectedItem: JQuery = $(this.$input.find('option:selected')) as JQuery + private findAddresCallback: () => void + private resultProcessedCallback: () => void + + constructor(findAddress: FindAddress, $section: JQuery, $input: JQuery, inputType: string, findAddresCallback: () => void, resultProcessedCallback: () => void) { + super(findAddress, $section, $input, inputType) + + this.findAddresCallback = findAddresCallback + this.resultProcessedCallback = resultProcessedCallback + + this.setEventListener() + } + + protected abstract createOption: (result: AddressResult | RegionResult) => string + + private setEventListener = () => this.$input.on('change', this.selectItem) + + protected getPromptText = (resultsLength: number): string => `${resultsLength} ${resultsLength == 1 ? this.resultTextPromptOptions.textSingle : this.resultTextPromptOptions.textPlural}` + + protected getOptions = (results: AddressResult[] | RegionResult[]): string[] => { + return [ + ``, + ...results.map((result: AddressResult | RegionResult): string => this.createOption(result)) + ] + } + + protected selectItem = (): void => { + const $selectedItem: JQuery = $(this.$input.find('option:selected')) as JQuery + + if ($selectedItem.val() === '') return + + this.itemSelected($selectedItem) + } + + protected itemSelected = ($selectedItem: JQuery): void => { + this.clearError() + + this.$selectedItem = $selectedItem + + this.findAddresCallback() + } + + processResult = (results: AddressResult[] | RegionResult[]): void => { + this.clearSelection() + + this.getOptions(results).forEach((option: string) => this.$input.append(option)) + + this.resultProcessedCallback() + } + + clearSelection = (): void => { + this.clearError() + + this.$input.empty() + } + + focus = (): JQuery => this._focus(this.$input) +} + +abstract class FindAddressTextSection extends FindAddressSection implements FindAddressTextSectionInterface { + protected $sectionText: JQuery + protected $changeSectionLink: JQuery + protected abstract hiddenInputs: {[key: string]: JQuery} + + constructor (findAddress: FindAddress, $section: JQuery, $sectionText: JQuery, $changeSectionLink: JQuery, findAddresCallback: () => void) { + super(findAddress, $section) + + this.$sectionText = $sectionText + this.$changeSectionLink = $changeSectionLink + + this.setEventListener(findAddresCallback) + } + + private setEventListener = (findAddresCallback: () => void) => { + this.$changeSectionLink.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + this.clearSelectedItems() + + findAddresCallback() + }) + } + + clearSelectedItems = (): void => { + this.$sectionText.text('') + + Object.entries(this.hiddenInputs).forEach(([, $hiddenInput]) => $hiddenInput.val('')) + } + + focus = (): JQuery => this._focus(this.$changeSectionLink) +} + +class PostcodeSearch extends FindAddressInputSection implements PostcodeSearchInterface { + private $findAddressButton: JQuery = $('#find-address-button') + private postcodeValue = String(this.$input.val() || '') + + constructor (findAddress: FindAddress) { + super( + findAddress, + $('#postcode-search'), + $('.postcode-entry'), + 'input' + ) + + this.setEventListener() + } + + private setEventListener = (): void => { + this.$findAddressButton.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + this.clearError() + + const destructuredPostCode = this.destructurePostCode() + + if (destructuredPostCode.valid) { + this.postcodeValue = destructuredPostCode.fullPostcode + this.findAddress.findAddressFromPostcode() + } else { + this.showError() + } + }) + } + + private destructurePostCode(): PostcodeResult { + const input: string = String(this.$input.val() || '').trim().toUpperCase() + const regEx = /^(([A-Z][A-Z]{0,1})([0-9][A-Z0-9]{0,1})) {0,}(([0-9])([A-Z]{2}))$/i + const matches: RegExpMatchArray | null = input.match(regEx) + + let result: PostcodeResult = { valid: false, input } + + if (matches !== null) { + result = { + ...result, + valid: true, + fullPostcode: `${matches[1]} ${matches[4]}` + } + } + + return result + } + + getPostcode = (): string => this.postcodeValue + + focus = (): JQuery => this._focus(this.$input) +} + +class PostcodeChange extends FindAddressSection implements PostcodeChangeInterface { + private $postcodeOnView: JQuery = $('#postcode-on-view') + private $changePostcodeLink: JQuery = $('#change-input-1') + + constructor (findAddress: FindAddress) { + super(findAddress, $('#postcode-change')) + + this.setEventListener() + } + + private setEventListener = (): void => { + this.$changePostcodeLink.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + this.findAddress.changePostcode() + }) + } + + setPostcode = (postcode: string): void => { + this.$postcodeOnView.text(postcode) + } + + focus = (): JQuery => this._focus(this.$changePostcodeLink) +} + +class SelectAnAddress extends FindAddressSelectSection implements SelectAnAddressInterface { + constructor (findAddress: FindAddress) { + super( + findAddress, + $('#select-an-address'), + $('#address-results-container'), + 'select', + findAddress.addressSelected, + findAddress.addressesFound + ) + } + + protected createOption = (result: AddressResult | RegionResult): string => { + const addressResult: AddressResult = result as AddressResult + + return ` + + ` + } + + getAddressElements = (): AddressElements => { + return { + summaryLine: String(this.$selectedItem.val() || ''), + addressLine1: this.$selectedItem.data('addressLine1'), + addressLine2: this.$selectedItem.data('addressLine2'), + addressTown: this.$selectedItem.data('addressTown'), + addressPostcode: this.$selectedItem.data('addressPostcode') + } + } +} + +class SelectedAddress extends FindAddressTextSection implements SelectedAddressInterface { + protected hiddenInputs: HiddenAddressInputs = { + addressLine1: $('#address-line-1'), + addressLine2: $('#address-line-2'), + addressTown: $('#address-town'), + addressCounty: $('address-county') + } + + constructor (findAddress: FindAddress) { + super( + findAddress, + $('#selected-address'), + $('#address-text'), + $('#change-input-2'), + findAddress.changeAddress + ) + } + + populateSelectedAddress = (addressElements: AddressElements): void => { + this.$sectionText.text(`${addressElements.summaryLine} ${addressElements.addressPostcode}`) + + this.hiddenInputs.addressLine1.val(addressElements.addressLine1) + this.hiddenInputs.addressLine2.val(addressElements.addressLine2) + this.hiddenInputs.addressTown.val(addressElements.addressTown) + } +} + +class SelectARegion extends FindAddressSelectSection implements SelectARegionInterface { + constructor (findAddress: FindAddress) { + super( + findAddress, + $('#select-a-region'), + $('#regions-results-container'), + 'select', + findAddress.regionSelected, + findAddress.regionsFound + ) + } + + protected createOption = (result: AddressResult | RegionResult): string => { + const regionResult: RegionResult = result as RegionResult + + return ` + + ` + } + + private regionOptions = (): JQuery => this.$input.find('option[value][value!=""]') as JQuery + + getRegionElements = (): RegionElements => { + return { + addressRegion: this.$selectedItem.data('addressRegion'), + addressRegionCode: this.$selectedItem.data('addressRegionCode') + } + } + + isOneResult = (): boolean => { + return this.regionOptions().length == 1 + } + + selectOnlyRegion = (): void => { + const $selectedRegion: JQuery = this.regionOptions() + + $selectedRegion.prop('selected', true) + + this.itemSelected($selectedRegion) + } + + resetSelectedRegion = (): void => { + this.$selectedItem.prop('selected', false) + } +} + +class SelectedRegion extends FindAddressTextSection implements SelectedRegionInterface { + protected hiddenInputs: HiddenRegionInputs = { + addressRegion: $('#address-region'), + addressRegionCode: $('#address-region-code') + } + + constructor (findAddress: FindAddress) { + super( + findAddress, + $('#selected-region'), + $('#region-text'), + $('#change-input-3'), + findAddress.changeRegion + ) + } + + populateSelectedRegion = (regionElements: RegionElements): void => { + this.$sectionText.text(regionElements.addressRegion) + + this.hiddenInputs.addressRegion.val(regionElements.addressRegion) + this.hiddenInputs.addressRegionCode.val(regionElements.addressRegionCode) + } + + toggleChangeLinkVisibility = (isShown: boolean): void => { + let tabindex + + if (isShown) { + this.$changeSectionLink.removeClass('govuk-visually-hidden') + tabindex = 0 + } else { + this.$changeSectionLink.addClass('govuk-visually-hidden') + tabindex = -1 + } + + this.$changeSectionLink.attr('tabIndex', tabindex) + } +} + +enum StateToView { + postcodeSearch = 1, + selectAddress = 2, + addressSelectedOneRegion = 3, + selectRegion = 4, + regionSelected = 5 +} + +class FindAddress implements FindAddressInterface { + private postcodeSearch: PostcodeSearch + private postcodeChange: PostcodeChange + private selectAnAddress: SelectAnAddress + private selectedAddress: SelectedAddress + private selectARegion?: SelectARegion + private selectedRegion?: SelectedRegion + + constructor() { + this.postcodeSearch = new PostcodeSearch(this) + this.postcodeChange = new PostcodeChange(this) + this.selectAnAddress = new SelectAnAddress(this) + this.selectedAddress = new SelectedAddress(this) + + if ($('[data-module="find-region"]').length) { + this.selectARegion = new SelectARegion(this) + this.selectedRegion = new SelectedRegion(this) + } + } + + private updateViewAndFocus = (state: number): void => { + this.updateView(state) + this.updateFocus(state) + } + + private updateView = (state: number): void => { + this.postcodeSearch.toggleSectionVisibility(state === 1) + this.postcodeChange.toggleSectionVisibility(state === 2) + this.selectAnAddress.toggleSectionVisibility(state === 2) + this.selectedAddress.toggleSectionVisibility(state >= 3) + this.selectARegion?.toggleSectionVisibility(state === 4) + this.selectedRegion?.toggleSectionVisibility(state === 3 || state === 5) + this.selectedRegion?.toggleChangeLinkVisibility(state === 5) + } + + private updateFocus = (state: number): void => { + switch (state) { + case 1: + this.postcodeSearch.focus() + break + case 2: + this.selectAnAddress.focus() + break + case 3: + this.selectAnAddress.focus() + break + case 4: + this.selectARegion?.focus() + break + case 5: + this.selectedRegion?.focus() + break + default: + break + } + } + + findAddressFromPostcode = () => { + const postcode = this.postcodeSearch.getPostcode() + + this.postcodeChange.setPostcode(this.postcodeSearch.getPostcode()) + callAPI( + this.selectAnAddress, + `/api/v2/postcodes/${postcode}` + ) + } + + addressesFound = () => this.updateViewAndFocus(StateToView.selectAddress) + + changePostcode = () => { + this.updateViewAndFocus(StateToView.postcodeSearch) + this.selectAnAddress.clearSelection() + } + + addressSelected = () => { + this.selectedAddress.populateSelectedAddress(this.selectAnAddress.getAddressElements()) + + if (this.selectARegion !== undefined) { + const postcode = this.postcodeSearch.getPostcode() + + callAPI( + this.selectARegion, + `/api/v2/find-region-postcode/${postcode}` + ) + } else { + this.updateViewAndFocus(StateToView.addressSelectedOneRegion) + } + } + + regionsFound = () => { + if (this.selectARegion !== undefined) { + if (this.selectARegion.isOneResult()) { + this.selectARegion.selectOnlyRegion() + this.regionSelected() + } else { + this.updateViewAndFocus(StateToView.selectRegion) + } + } + } + + changeAddress = () => { + this.selectAnAddress.clearSelection() + + if (this.selectARegion !== undefined && this.selectedRegion !== undefined) { + this.selectARegion.clearSelection() + this.selectedRegion.clearSelectedItems() + } + + this.updateViewAndFocus(StateToView.postcodeSearch) + } + + regionSelected = () => { + if (this.selectARegion !== undefined && this.selectedRegion !== undefined) { + + this.selectedRegion.populateSelectedRegion(this.selectARegion.getRegionElements()) + + if (this.selectARegion.isOneResult()) { + this.updateViewAndFocus(StateToView.addressSelectedOneRegion) + } else { + this.updateViewAndFocus(StateToView.regionSelected) + } + } + } + + changeRegion = () => { + if (this.selectARegion !== undefined && this.selectedRegion !== undefined) { + this.selectARegion.resetSelectedRegion() + this.selectedRegion.clearSelectedItems() + + this.updateViewAndFocus(StateToView.selectRegion) + } + } +} + +$(() => { + if ($('[data-module="find-address"]').length) new FindAddress() +}) diff --git a/app/typescript/facilitiesManagement/integerInput.ts b/app/typescript/facilitiesManagement/integerInput.ts new file mode 100644 index 0000000000..1a0fe8f6d9 --- /dev/null +++ b/app/typescript/facilitiesManagement/integerInput.ts @@ -0,0 +1,9 @@ +const limitInputToInteger = (): void => { + $('.ccs-integer-field').on('keypress', (event: JQuery.KeyPressEvent) => { + if ((event.key < '0' || event.key > '9')) event.preventDefault() + }) +} + +$(() => { + if ($('.ccs-integer-field').length) limitInputToInteger() +}) diff --git a/app/typescript/facilitiesManagement/numberWithCommas.ts b/app/typescript/facilitiesManagement/numberWithCommas.ts new file mode 100644 index 0000000000..34e8f062ae --- /dev/null +++ b/app/typescript/facilitiesManagement/numberWithCommas.ts @@ -0,0 +1,30 @@ +const updateNumberWithCommas = ($input: JQuery): void => { + const number = String($input.val() || '') + const numberString = number.replace(/,/g, '') + + $input.val(numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ',')) +} + +const updateNumberWithoutCommas = ($input: JQuery): void => { + const number = String($input.val() || '') + + $input.val(number.toString().replace(/,/g, '')) +} + +const showNumberWithCommas = (): void => { + const $input: JQuery = $('.ccs-number-field') + + updateNumberWithCommas($input) + + $input.on('keyup', (event: JQuery.KeyUpEvent) => { + updateNumberWithCommas($(event.currentTarget)) + }) + + $('form').on('submit', () => { + updateNumberWithoutCommas($input) + }) +} + +$(() => { + if ($('.ccs-number-field').length) showNumberWithCommas() +}) diff --git a/app/typescript/facilitiesManagement/procurements/chooseServicesForBuilding.ts b/app/typescript/facilitiesManagement/procurements/chooseServicesForBuilding.ts new file mode 100644 index 0000000000..c0dd3ac191 --- /dev/null +++ b/app/typescript/facilitiesManagement/procurements/chooseServicesForBuilding.ts @@ -0,0 +1,33 @@ +const numberOfCheckedServices = (): number => { + return $('input.procurement-building__input:checked').length +} + +const checkAllSelected = (numberOfCheckboxes: number): void => { + $('#box-all').prop('checked', numberOfCheckboxes === numberOfCheckedServices()) +} + +const checkAll = (isChecked: boolean): void => { + $('.procurement-building__input').each((_index: number, element: HTMLElement) => { + $(element).prop('checked', isChecked) + }) + $('#box-all').prop('checked', isChecked) +} + +const initChooseServicesForBuilding = (): void => { + if ($('.buildings_and_services').length && $('.ccs-select-all').length) { + const numberOfCheckboxes: number = $('input.procurement-building__input').length + + checkAllSelected(numberOfCheckboxes) + + $('.govuk-checkboxes').each((_index: number, element: HTMLElement) => { + $(element).on('click', () => checkAllSelected(numberOfCheckboxes)) + }) + + $('#box-all').on('click', () => { + checkAll(numberOfCheckboxes > numberOfCheckedServices()) + }) + } +} + +$(() => initChooseServicesForBuilding()) + diff --git a/app/typescript/facilitiesManagement/procurements/contractExtensions.ts b/app/typescript/facilitiesManagement/procurements/contractExtensions.ts new file mode 100644 index 0000000000..0aa4b96b3f --- /dev/null +++ b/app/typescript/facilitiesManagement/procurements/contractExtensions.ts @@ -0,0 +1,360 @@ +type NormalisationFactors = { + years: number + months: number + weeks: number +} + +type ButtonInterface = { + $button: JQuery + hideButton: () => void + showButton: () => void +} + +type YearsAndMonthsInputInterface = { + $years: JQuery + $months: JQuery + yearsAndMonthsInomplete: () => boolean + totalPeriod: () => number +} + +type ExtensionPeriodInterface = { + $years: JQuery + $months: JQuery + $removeButton: ContractPeriodButton + showExtensionPeriod: () => void + hideExtensionPeriod: () => void + isVisible: () => boolean + isRequired: () => boolean +} + +type ContractPeriodInterface = { + $mobilisationPeriodRequiredTrue: JQuery + $mobilisationPeriodRequiredFalse: JQuery + $periodInput: JQuery + $extensionsRequiredTrue: JQuery + $extensionsRequiredFalse: JQuery + extensions: ExtensionPeriod[] + extensionChecked: () => boolean + allPeriodInputsComplete: () => boolean + calculateTotalContractPeriod: () => void + getTotalTimeRemaining: () => number + timeRemaining: () => [number, number] +} + +type AddExtensionPeriodButtonInterface = { + ableToAddPeriod: () => boolean + updateButtonVisibility: () => void + updateButtonText: () => void + updateButtonState: () => void +} + +class ContractPeriodButton implements ButtonInterface { + $button: JQuery + + constructor($button: JQuery) { + this.$button = $button + } + + hideButton = (): void => { + this.$button.addClass('govuk-visually-hidden') + this.$button.attr('tabindex', -1) + } + + showButton = (): void => { + this.$button.removeClass('govuk-visually-hidden') + this.$button.attr('tabindex', 0) + } +} + +class YearsAndMonthsNormalisation { + static normalisationFactors: NormalisationFactors = { + years: 156, + months: 13, + weeks: 3 + } + + static normaliseTimeLength = (inputElement: JQuery, factor: 'years' | 'months' | 'weeks'): number => parseInt(String(inputElement.val()), 10) * this.normalisationFactors[factor] +} + +class YearsAndMonthsInput implements YearsAndMonthsInputInterface { + $years: JQuery + $months: JQuery + + constructor($years: JQuery, $months: JQuery) { + this.$years = $years + this.$months = $months + } + + private normalisedYears = (): number => YearsAndMonthsNormalisation.normaliseTimeLength(this.$years, 'years') + + private normalisedMonths = (): number => YearsAndMonthsNormalisation.normaliseTimeLength(this.$months, 'months') + + yearsAndMonthsInomplete = (): boolean => { + const years: number = this.normalisedYears() + const months: number = this.normalisedMonths() + + return Number.isNaN(years) || Number.isNaN(months) || (years + months) === 0 + } + + totalPeriod = (): number => { + return this.normalisedYears() + this.normalisedMonths() + } +} + +class InitialCallOffPeriod extends YearsAndMonthsInput { + constructor(framework: string) { + super($(`#facilities_management_${framework}_procurement_initial_call_off_period_years`), $(`#facilities_management_${framework}_procurement_initial_call_off_period_months`)) + } +} + +class ExtensionPeriod extends YearsAndMonthsInput implements ExtensionPeriodInterface { + $removeButton: ContractPeriodButton + + private $container: JQuery + private $required: JQuery + + constructor(framework: string, extension: number) { + super($(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_years`), $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_months`)) + this.$container = $(`#extension-${extension}-container`) + this.$required = $(`#facilities_management_${framework}_procurement_call_off_extensions_attributes_${extension}_extension_required`) + this.$removeButton = new ContractPeriodButton($(`#extension-${extension}-remove-button`)) + } + + showExtensionPeriod = (): void => { + this.$container.removeClass('govuk-visually-hidden') + this.$years.attr('tabindex', 0) + this.$months.attr('tabindex', 0) + this.$required.val('true') + this.$removeButton.showButton() + } + + hideExtensionPeriod = (): void => { + this.$container.addClass('govuk-visually-hidden') + this.resetInput(this.$years) + this.resetInput(this.$months) + this.$required.val('false') + this.$container.find('.govuk-error-message').each((_, errorMessage: HTMLElement) => { + $(errorMessage).remove() + }) + this.$removeButton.hideButton() + } + + private resetInput = ($element: JQuery) => { + $element.attr('tabindex', -1) + $element.val('') + $element.removeClass('govuk-input--error') + } + + isVisible = (): boolean => this.$container.hasClass('govuk-visually-hidden') + + isRequired = (): boolean => this.$required.val() === 'true' +} + +class ContractPeriod implements ContractPeriodInterface { + $mobilisationPeriodRequiredTrue: JQuery + $mobilisationPeriodRequiredFalse: JQuery + $periodInput: JQuery = $('.period-input') + $extensionsRequiredTrue: JQuery + $extensionsRequiredFalse: JQuery + extensions: ExtensionPeriod[] + + private totalContractPeriod = 0 + private initialCallOffPeriod: InitialCallOffPeriod + private $mobilisationPeriod: JQuery + + constructor(framework: string) { + this.initialCallOffPeriod = new InitialCallOffPeriod(framework) + this.$mobilisationPeriodRequiredTrue = $(`#facilities_management_${framework}_procurement_mobilisation_period_required_true`) + this.$mobilisationPeriodRequiredFalse = $(`#facilities_management_${framework}_procurement_mobilisation_period_required_false`) + this.$mobilisationPeriod = $(`#facilities_management_${framework}_procurement_mobilisation_period`) + this.$extensionsRequiredTrue = $(`#facilities_management_${framework}_procurement_extensions_required_true`) + this.$extensionsRequiredFalse = $(`#facilities_management_${framework}_procurement_extensions_required_false`) + this.extensions = [...Array(4).keys()].map((extension: number) => new ExtensionPeriod(framework, extension)) + } + + private mobilisationPeriodChecked = (): boolean => this.$mobilisationPeriodRequiredTrue.is(':checked') + + private mobilisationPeriod = (): number => YearsAndMonthsNormalisation.normaliseTimeLength(this.$mobilisationPeriod, 'weeks') + + extensionChecked = (): boolean => this.$extensionsRequiredTrue.is(':checked') + + fourthExtensionRequired = (): boolean => this.extensions[3].isRequired() + + allPeriodInputsComplete = (): boolean => { + if (this.initialCallOffPeriod.yearsAndMonthsInomplete()) return false + + if (this.mobilisationPeriodChecked() && !(this.mobilisationPeriod() > 0)) return false + + let extensionsCompleted = true + + if (this.extensionChecked()) { + this.extensions.forEach((extension: ExtensionPeriod) => { + if (extension.isVisible()) return + + extensionsCompleted = !extension.yearsAndMonthsInomplete() + + if (!extensionsCompleted) return + }) + } + + return extensionsCompleted + } + + calculateTotalContractPeriod = (): void => { + let totalPeriod = 0 + + totalPeriod += this.initialCallOffPeriod.totalPeriod() + + totalPeriod += this.mobilisationPeriodChecked() ? this.mobilisationPeriod() : 0 + + if (this.extensionChecked()) { + this.extensions.forEach((extension: ExtensionPeriod) => { + if (extension.isVisible()) return + + totalPeriod += extension.totalPeriod() + }) + } + + this.totalContractPeriod = totalPeriod + } + + getTotalTimeRemaining = (): number => 1560 - this.totalContractPeriod + + timeRemaining = (): [number, number] => { + const totalTimeRemaining: number = this.getTotalTimeRemaining() + + const years: number = Math.floor(totalTimeRemaining / YearsAndMonthsNormalisation.normalisationFactors.years) + const months: number = Math.floor((totalTimeRemaining % YearsAndMonthsNormalisation.normalisationFactors.years) / YearsAndMonthsNormalisation.normalisationFactors.months) + + return [years, months] + } +} + +class $AddExtensionPeriodButton extends ContractPeriodButton implements AddExtensionPeriodButtonInterface { + private contractPeriod: ContractPeriod + + constructor(contractPeriod: ContractPeriod) { + super($('#add-contract-extension-button')) + this.contractPeriod = contractPeriod + } + + ableToAddPeriod = (): boolean => { + if (this.forthExtensionRequired()) return false + if (!this.contractPeriod.allPeriodInputsComplete()) return false + + this.contractPeriod.calculateTotalContractPeriod() + if (this.noTimePeriodLeftToAdd()) return false + + return true + } + + updateButtonVisibility = (): void => { + if (!this.contractPeriod.extensionChecked() || this.forthExtensionRequired() || this.noTimePeriodLeftToAdd()) { + this.hideButton() + } else { + this.showButton() + } + } + + updateButtonText = (): void => { + this.$button.text(this.getButtonText()) + } + + private getButtonText = (): string => { + const timeRemainingParts: number[] = this.contractPeriod.timeRemaining() + const years: number = timeRemainingParts[0] + const months: number = timeRemainingParts[1] + + let text = 'Add another extension period (' + + if (years > 0) text += `${years} year` + if (years > 1) text += 's' + if (years > 0 && months > 0) text += ' and ' + if (months > 0) text += `${months} month` + if (months > 1) text += 's' + + return `${text} remaining)` + } + + private forthExtensionRequired = (): boolean => this.contractPeriod.fourthExtensionRequired() + + private noTimePeriodLeftToAdd = (): boolean => this.contractPeriod.getTotalTimeRemaining() < 13 + + updateButtonState = (): void => { + if (this.contractPeriod.allPeriodInputsComplete()) { + this.contractPeriod.calculateTotalContractPeriod() + this.updateButtonVisibility() + this.updateButtonText() + } + } +} + +const initContractExtenesions = (): void => { + const $callOffExtensionsContainer = $('#call-off-extensions') + + if ($callOffExtensionsContainer.length > 0) { + const framework: string = $callOffExtensionsContainer.data().framework + const contractPeriod: ContractPeriod = new ContractPeriod(framework) + const addExtensionPeriodButton: $AddExtensionPeriodButton = new $AddExtensionPeriodButton(contractPeriod) + + contractPeriod.$extensionsRequiredTrue.on('click', () => { + contractPeriod.extensions[0].showExtensionPeriod() + addExtensionPeriodButton.hideButton() + if (addExtensionPeriodButton.ableToAddPeriod()) addExtensionPeriodButton.showButton() + }) + + contractPeriod.$extensionsRequiredFalse.on('click', () => { + contractPeriod.extensions.forEach((extension: ExtensionPeriod) => extension.hideExtensionPeriod()) + addExtensionPeriodButton.hideButton() + }) + + contractPeriod.extensions.forEach((extension: ExtensionPeriod, index: number) => { + extension.$removeButton.$button.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + extension.hideExtensionPeriod() + contractPeriod.extensions[index - 1].$removeButton.showButton() + + if (addExtensionPeriodButton.ableToAddPeriod()) { + addExtensionPeriodButton.updateButtonVisibility() + addExtensionPeriodButton.updateButtonText() + } + }) + }) + + addExtensionPeriodButton.$button.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + if (addExtensionPeriodButton.ableToAddPeriod()) { + const nextExtensionIndex = 4 - $('.extension-container.govuk-visually-hidden').length + + contractPeriod.extensions[nextExtensionIndex].showExtensionPeriod() + contractPeriod.extensions[nextExtensionIndex - 1].$removeButton.hideButton() + + addExtensionPeriodButton.updateButtonVisibility() + addExtensionPeriodButton.updateButtonText() + } + }) + + contractPeriod.$periodInput.on('keyup', () => { + addExtensionPeriodButton.updateButtonState() + }) + + contractPeriod.$mobilisationPeriodRequiredTrue.on('click', () => { + addExtensionPeriodButton.updateButtonState() + }) + + contractPeriod.$mobilisationPeriodRequiredFalse.on('click', () => { + addExtensionPeriodButton.updateButtonState() + }) + + if (addExtensionPeriodButton.ableToAddPeriod()) { + addExtensionPeriodButton.updateButtonText() + addExtensionPeriodButton.updateButtonVisibility() + } else { + addExtensionPeriodButton.hideButton() + } + } +} + +$(() => initContractExtenesions()) diff --git a/app/typescript/facilitiesManagement/procurements/resultsToggle.ts b/app/typescript/facilitiesManagement/procurements/resultsToggle.ts new file mode 100644 index 0000000000..769817c454 --- /dev/null +++ b/app/typescript/facilitiesManagement/procurements/resultsToggle.ts @@ -0,0 +1,26 @@ +const initResultsToggle = (): void => { + const $resultsFilterButton: JQuery = $('#results-filter-button') + + $resultsFilterButton.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + const $requirementsPlane: JQuery = $('#requirements-list') + const $suppliersPlane: JQuery = $('#supplier-lot-list__container') + + const isHidden: boolean = $requirementsPlane.is(':hidden') + const currentText: string = $resultsFilterButton.text() + + if (isHidden) { + $requirementsPlane.show() + $suppliersPlane.addClass('govuk-grid-column-two-thirds').removeClass('govuk-grid-column-full') + } else { + $requirementsPlane.hide() + $suppliersPlane.addClass('govuk-grid-column-full').removeClass('govuk-grid-column-two-thirds') + } + + $resultsFilterButton.text($resultsFilterButton.attr('alt-text') || '') + $resultsFilterButton.attr('alt-text', currentText) + }) +} + +$(() => initResultsToggle()) \ No newline at end of file diff --git a/app/typescript/facilitiesManagement/procurements/selectRegion.ts b/app/typescript/facilitiesManagement/procurements/selectRegion.ts new file mode 100644 index 0000000000..0b4152f521 --- /dev/null +++ b/app/typescript/facilitiesManagement/procurements/selectRegion.ts @@ -0,0 +1,49 @@ +const showAndHideElements = ($elementToShow: JQuery, $elementToHide: JQuery) => { + $elementToShow.attr('tabIndex', 0) + $elementToHide.attr('tabIndex', -1) + + const elementToShow = $elementToShow.get(0) + if(elementToShow) elementToShow.focus() +} + +const selectRegion = ($regionDropDown: JQuery, $changeRegion: JQuery): void => { + const selectedRegionText = $regionDropDown.find(':selected').text() + + if (selectedRegionText) { + showAndHideElements($changeRegion, $regionDropDown) + + $('.govuk-error-summary').hide() + $('#address_region-error').hide() + $('#address_region-form-group').removeClass('govuk-form-group--error') + $('#building-region').text(selectedRegionText) + $('#select-region').hide() + $('#region-selection').show() + } +} + +const changeRegion = ($regionDropDown: JQuery, $changeRegion: JQuery): void => { + showAndHideElements($regionDropDown, $changeRegion) + + $regionDropDown.prop('selectedIndex', 0) + $('#region-selection').hide() + $('#select-region').show() +} + +const initSelectRegion = (): void => { + if ($('#building-missing-region').length < 1) return + + const $regionDropDown: JQuery = $('#facilities_management_building_address_region') + const $changeRegion: JQuery = $('#change-region') + + $regionDropDown.on('change', () => { + selectRegion($regionDropDown, $changeRegion) + }) + + $changeRegion.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + changeRegion($regionDropDown, $changeRegion) + }) +} + +$(() => initSelectRegion()) diff --git a/app/typescript/facilitiesManagement/rm3830/addNestedAttributes.ts b/app/typescript/facilitiesManagement/rm3830/addNestedAttributes.ts new file mode 100644 index 0000000000..935d6bf218 --- /dev/null +++ b/app/typescript/facilitiesManagement/rm3830/addNestedAttributes.ts @@ -0,0 +1,215 @@ +type NestedAttributesFieldsArguments = { + $button: JQuery + nestedAttributeRowClasses: NestedAttributeRowClasses + rowLabelText: string + removeButtonMode: RemoveButtonModes +} + +type NestedAttributeRowClasses = { + rowClass: string + rowNumberClass: string + removeButtonClass: string + destroyRowClass: string +} + +type RemoveButtonModes = 'all' | 'final' + +type NestedAttributeRowArguments = { + initiateRow: { + $row: JQuery + }, + generateRowHTML?: never +} | { + initiateRow?: never + generateRowHTML: { + fields: string + id: string + } +} + +interface NestedAttributeRowInterface { + updateRowLabelText: (text: string) => void + toggleRemoveButton: (isShown: boolean) => void +} + +interface AddNestedAttributeButtonInterface { + updateButtonText: (numberOfItems: number) => void +} + +interface NestedAttributesFieldsInterface { + updateAll: () => void + addNestedAttributeRow: (nestedAttributeRowArguments: NestedAttributeRowArguments) => void + removeNestedAttributeRow: (nestedAttributeRowToRemove: NestedAttributeRow) => void +} + +class NestedAttributeRow implements NestedAttributeRowInterface { + private $row: JQuery + private $rowLabel: JQuery + private $rowRemoveButton: JQuery + private $destoryRowInput: JQuery + + constructor(nestedAttributesFields: NestedAttributesFields, nestedAttributeRowClasses: NestedAttributeRowClasses, nestedAttributeRowArguments: NestedAttributeRowArguments) { + if (nestedAttributeRowArguments.initiateRow !== undefined) { + this.$row = nestedAttributeRowArguments.initiateRow.$row + } else { + const time = new Date().getTime() + const regexp = new RegExp(nestedAttributeRowArguments.generateRowHTML.id, 'g') + + this.$row = $(nestedAttributeRowArguments.generateRowHTML.fields.replace(regexp, String(time))) + } + + this.$rowLabel = this.$row.find(`.${nestedAttributeRowClasses.rowNumberClass}`) as JQuery + this.$rowRemoveButton = this.$row.find(`.${nestedAttributeRowClasses.removeButtonClass}`) as JQuery + this.$destoryRowInput = this.$row.find(`.${nestedAttributeRowClasses.destroyRowClass}`) as JQuery + + if (nestedAttributeRowArguments.generateRowHTML !== undefined) { + const updatedFields: HTMLElement | undefined = this.$row.get(0) + + if (updatedFields !== undefined) $('.fields').append(updatedFields) + } + + this.setEventListeners(nestedAttributesFields) + } + + private setEventListeners = (nestedAttributesFields: NestedAttributesFields) => { + this.$rowRemoveButton.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + this.$destoryRowInput.val('true') + this.$row.hide() + this.$row.removeClass() + this.$rowRemoveButton.off() + + nestedAttributesFields.removeNestedAttributeRow(this) + + nestedAttributesFields.updateAll() + }) + + } + + updateRowLabelText = (text: string): void => { + this.$rowLabel.text(text) + } + + toggleRemoveButton = (isShown: boolean): void => { + isShown ? this.$rowRemoveButton.show() : this.$rowRemoveButton.hide() + } +} + +class AddNestedAttributeButton implements AddNestedAttributeButtonInterface { + private $button: JQuery + private id: string + private fields: string + private buttonText: string + + constructor($button: JQuery, nestedAttributesFields: NestedAttributesFields) { + this.$button = $button + this.id = $button.data('id') + this.fields = $button.data('fields') + this.buttonText = $button.data('button-text') + this.setEventListeners(nestedAttributesFields) + } + + private setEventListeners = (nestedAttributesFields: NestedAttributesFields): void => { + this.$button.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + nestedAttributesFields.addNestedAttributeRow({ + generateRowHTML: { + fields: this.fields, + id: this.id + } + }) + + nestedAttributesFields.updateAll() + }) + } + + updateButtonText = (numberOfItems: number): void => { + this.$button.text(this.buttonText.replace('', String(numberOfItems))) + } +} + +class NestedAttributesFields implements NestedAttributesFieldsInterface { + private maxNumberOfItems = 99 + private addNestedAttributeButton: AddNestedAttributeButton + private nestedAttributeRows: NestedAttributeRow[] = [] + private nestedAttributeRowClasses: NestedAttributeRowClasses + private rowLabelText: string + private removeButtonMode: RemoveButtonModes + + constructor(nestedAttributesFieldsArguments: NestedAttributesFieldsArguments) { + this.addNestedAttributeButton = new AddNestedAttributeButton(nestedAttributesFieldsArguments.$button, this) + this.nestedAttributeRowClasses = nestedAttributesFieldsArguments.nestedAttributeRowClasses + this.rowLabelText = nestedAttributesFieldsArguments.rowLabelText + this.removeButtonMode = nestedAttributesFieldsArguments.removeButtonMode + + $(`.${nestedAttributesFieldsArguments.nestedAttributeRowClasses.rowClass}`).each((_index: number, row: HTMLElement) => { + this.addNestedAttributeRow({ + initiateRow: { + $row: $(row) + } + }) + }) + + this.updateAll() + } + + private getNumberOfRows = (): number => this.nestedAttributeRows.length + + private updateRowAndButtonNumbers = (): void => { + this.nestedAttributeRows.forEach((nestedAttributeRow: NestedAttributeRow, index: number) => nestedAttributeRow.updateRowLabelText(`${this.rowLabelText} ${index + 1}`)) + this.addNestedAttributeButton.updateButtonText(this.maxNumberOfItems - this.getNumberOfRows()) + } + + private updateRemoveButtonVisibilities = (): void => { + if (this.removeButtonMode === 'all') { + this.nestedAttributeRows.forEach((nestedAttributeRow: NestedAttributeRow) => nestedAttributeRow.toggleRemoveButton(this.getNumberOfRows() > 1)) + } else { + this.nestedAttributeRows.forEach((nestedAttributeRow: NestedAttributeRow, index: number) => nestedAttributeRow.toggleRemoveButton(index > 0 && index + 1 ===this.getNumberOfRows())) + } + } + + updateAll = (): void => { + this.updateRemoveButtonVisibilities() + this.updateRowAndButtonNumbers() + } + + addNestedAttributeRow = (nestedAttributeRowArguments: NestedAttributeRowArguments): void => { + if (this.getNumberOfRows() < this.maxNumberOfItems) this.nestedAttributeRows.push(new NestedAttributeRow(this, this.nestedAttributeRowClasses, nestedAttributeRowArguments)) + } + + removeNestedAttributeRow = (nestedAttributeRowToRemove: NestedAttributeRow): void => { + this.nestedAttributeRows = this.nestedAttributeRows.filter((nestedAttributeRow: NestedAttributeRow) => nestedAttributeRow !== nestedAttributeRowToRemove) + } +} + +const initNestedAttributesFields = (): void => { + const pensionFund: NestedAttributesFieldsArguments = { + $button: $('.add-pension-button'), + nestedAttributeRowClasses: { + rowClass: 'pension-row', + rowNumberClass: 'pension-number', + removeButtonClass: 'remove-pension-record', + destroyRowClass: 'pension-destroy-box' + }, + rowLabelText: 'Pension fund name', + removeButtonMode: 'all' + } + + const lifts: NestedAttributesFieldsArguments = { + $button: $('.add-lift-button'), + nestedAttributeRowClasses: { + rowClass: 'lift-row', + rowNumberClass: 'lift-number', + removeButtonClass: 'remove-lift-record', + destroyRowClass: 'lift-destroy-box' + }, + rowLabelText: 'Lift', + removeButtonMode: 'final' + } + + if (pensionFund.$button.length > 0) new NestedAttributesFields(pensionFund) + if (lifts.$button.length > 0) new NestedAttributesFields(lifts) +} + +$(() => initNestedAttributesFields()) diff --git a/app/assets/javascripts/facilities-management/rm3830/admin/fm-admin-upload-prgress.js b/app/typescript/facilitiesManagement/rm3830/admin/adminUploadProgress.ts similarity index 75% rename from app/assets/javascripts/facilities-management/rm3830/admin/fm-admin-upload-prgress.js rename to app/typescript/facilitiesManagement/rm3830/admin/adminUploadProgress.ts index 5898cacfaa..55b5a21b3b 100644 --- a/app/assets/javascripts/facilities-management/rm3830/admin/fm-admin-upload-prgress.js +++ b/app/typescript/facilitiesManagement/rm3830/admin/adminUploadProgress.ts @@ -1,8 +1,8 @@ -const adminStateToProgress = { +const adminStateToProgress: StateToProgressWithProgressBar = { not_started: { progress: 10, wait: 500, - state: 'progress-0', + state: 'progress-0' }, in_progress: { progress: 10, @@ -34,17 +34,17 @@ const adminStateToProgress = { wait: 500, state: 'progress-4', colourClass: 'ccs-progress-bar--succeed', + isFinished: true }, failed: { progress: 100, wait: 500, state: 'progress-4', colourClass: 'ccs-progress-bar--fail', + isFinished: true }, -}; +} $(() => { - if ($('#admin-import').length) { - uploadFileImport.init(adminStateToProgress, 'published', 'failed'); - } -}); + if ($('#admin-import').length) new FileUploadProgressWithBar(adminStateToProgress, 'in_progress') +}) diff --git a/app/typescript/facilitiesManagement/rm3830/admin/managementReport.ts b/app/typescript/facilitiesManagement/rm3830/admin/managementReport.ts new file mode 100644 index 0000000000..5ef76b4d4b --- /dev/null +++ b/app/typescript/facilitiesManagement/rm3830/admin/managementReport.ts @@ -0,0 +1,14 @@ +const managementReportStateToProgress: StateToProgressWithoutProgressBar = { + generating_csv: { + wait: 15000, + isFinished: false + }, + completed: { + wait: 0, + isFinished: true + } +} + +$(() => { + if ($('#management-report-status').length && $('.management-report-state-generating_csv').length) new FileUploadProgressWithoutBar(managementReportStateToProgress, 'generating_csv') +}) diff --git a/app/assets/javascripts/facilities-management/rm3830/fm-bulk-upload-progress.js b/app/typescript/facilitiesManagement/rm3830/bulkUploadProgress.ts similarity index 77% rename from app/assets/javascripts/facilities-management/rm3830/fm-bulk-upload-progress.js rename to app/typescript/facilitiesManagement/rm3830/bulkUploadProgress.ts index 8da128bfae..2740cc1eff 100644 --- a/app/assets/javascripts/facilities-management/rm3830/fm-bulk-upload-progress.js +++ b/app/typescript/facilitiesManagement/rm3830/bulkUploadProgress.ts @@ -1,4 +1,4 @@ -const bulkUploadStateToProgress = { +const bulkUploadStateToProgress: StateToProgressWithProgressBar = { not_started: { progress: 10, wait: 1000, @@ -34,17 +34,17 @@ const bulkUploadStateToProgress = { wait: 1000, state: 'progress-4', colourClass: 'ccs-progress-bar--succeed', + isFinished: true }, data_import_failed: { progress: 100, wait: 1000, state: 'progress-4', colourClass: 'ccs-progress-bar--fail', + isFinished: true }, -}; +} $(() => { - if ($('#bulk-upload-import').length) { - uploadFileImport.init(bulkUploadStateToProgress, 'data_import_succeed', 'data_import_failed'); - } -}); + if ($('#bulk-upload-import').length) new FileUploadProgressWithBar(bulkUploadStateToProgress, 'in_progress') +}) diff --git a/app/typescript/facilitiesManagement/rm6232/admin/supplierDataSnapshot.ts b/app/typescript/facilitiesManagement/rm6232/admin/supplierDataSnapshot.ts new file mode 100644 index 0000000000..be7f03d3ed --- /dev/null +++ b/app/typescript/facilitiesManagement/rm6232/admin/supplierDataSnapshot.ts @@ -0,0 +1,23 @@ +const enableZipButton = (): void => { + $('#generate-supplier-zip-button').removeAttr('disabled') +} + +const disableZipButtonAndHideErrors = (): void => { + $('.govuk-error-message').remove() + $('.govuk-error-summary').remove() + $('.govuk-form-group').removeClass('govuk-form-group--error') + $('.govuk-input--error').removeClass('govuk-input--error') + $('#generate-supplier-zip-button').attr('disabled', 'disabled') +} + +const inputAttributes: string[] = ['date_dd', 'date_mm', 'date_yyyy', 'time_hh', 'time_mm'] + +const initSupplierDataSnapshot = (): void => { + inputAttributes.forEach((inputAttribute) => { + $(`#facilities_management_rm6232_admin_supplier_data_snapshot_snapshot_${inputAttribute}`).on('input', enableZipButton) + }) + + $('#generate-supplier-zip').on('submit', disableZipButtonAndHideErrors) +} + +$(() => initSupplierDataSnapshot()) \ No newline at end of file diff --git a/app/typescript/facilitiesManagement/uploadProgress.ts b/app/typescript/facilitiesManagement/uploadProgress.ts new file mode 100644 index 0000000000..c443764f6b --- /dev/null +++ b/app/typescript/facilitiesManagement/uploadProgress.ts @@ -0,0 +1,148 @@ +type StateToProgressDefaultOptions = { + wait: number +} + +type StateToProgressWithProgressBarDefaultOptions = StateToProgressDefaultOptions & { + progress: number + state: string +} + +type StateToProgressWithProgressBarNormalOptions = StateToProgressWithProgressBarDefaultOptions & { + colourClass?: never + isFinished?: never +} + +type StateToProgressWithProgressBarFinishedOptions = StateToProgressWithProgressBarDefaultOptions & { + colourClass: string + isFinished: boolean +} + +type StateToProgressWithoutProgressBarOptions = StateToProgressDefaultOptions & { + isFinished: boolean +} + +type StateToProgressWithProgressBar = { + [key: string]: StateToProgressWithProgressBarNormalOptions | StateToProgressWithProgressBarFinishedOptions +} + +type StateToProgressWithoutProgressBar = { + [key: string]: StateToProgressWithoutProgressBarOptions +} + +type FileUploadState = keyof StateToProgressWithProgressBar | keyof StateToProgressWithoutProgressBar + +type FileUploadResponseJSON = { + import_status: FileUploadState +} + +interface FileUploadProgressInterface { + url: string + updateCurrentState: (newStatus: FileUploadState) => void + stop: () => void + processImportStatus: () => void +} + +const checkImportProgress = (fileUploadProgress: FileUploadProgressInterface): void => { + $.ajax({ + type: 'GET', + url: fileUploadProgress.url, + dataType: 'json', + success(responseJSON: FileUploadResponseJSON) { + fileUploadProgress.updateCurrentState(responseJSON.import_status) + }, + error() { + fileUploadProgress.stop() + }, + complete() { + fileUploadProgress.processImportStatus() + }, + }) +} + +abstract class FileUploadProgress implements FileUploadProgressInterface { + readonly url = `${window.location.pathname}/progress` + protected continue = true + protected stateToProgress: StateToProgressWithProgressBar | StateToProgressWithoutProgressBar + protected currentState: StateToProgressWithProgressBarNormalOptions | StateToProgressWithProgressBarFinishedOptions | StateToProgressWithoutProgressBarOptions + + constructor(stateToProgress: StateToProgressWithProgressBar | StateToProgressWithoutProgressBar, initial_state: string) { + this.stateToProgress = stateToProgress + this.currentState = stateToProgress[initial_state] + + setTimeout(checkImportProgress.bind(null, this), 0) + } + + protected processComplete = (): void => { + window.location.reload() + } + + updateCurrentState = (newStatus: FileUploadState): void => { + this.currentState = this.stateToProgress[newStatus] + } + + stop = (): void => { + this.continue = false + } + + abstract processImportStatus: () => void +} + +class FileUploadProgressWithBar extends FileUploadProgress { + private $progressBar: JQuery = $('#upload-import-progress') + private $prgressStates: JQuery = $('.ccs-upload-progress-container > div') + + constructor(stateToProgress: StateToProgressWithProgressBar, initial_state: string) { + super(stateToProgress, initial_state) + } + + private updateProgressBar = (): void => { + this.$prgressStates.each(this.updateShownStatus) + } + + private updateShownStatus = (_index: number, element: HTMLElement): void => { + if ($(element).attr('id') === (this.currentState as StateToProgressWithProgressBarNormalOptions | StateToProgressWithProgressBarFinishedOptions).state) { + $(element).attr('aria-current', 'true') + $(element).addClass('govuk-!-font-weight-bold') + } else { + $(element).removeAttr('aria-current') + $(element).removeClass('govuk-!-font-weight-bold') + } + } + + processImportStatus = (): void => { + if (this.continue) { + this.$progressBar.attr('style', `width: ${(this.currentState as StateToProgressWithProgressBarNormalOptions | StateToProgressWithProgressBarFinishedOptions).progress}%`) + this.updateProgressBar() + let continueFunction = checkImportProgress.bind(null, this) + + if (this.currentState.isFinished) { + this.$progressBar.addClass((this.currentState as StateToProgressWithProgressBarFinishedOptions).colourClass) + continueFunction = this.processComplete + } + + setTimeout(continueFunction, this.currentState.wait) + } else { + this.processComplete() + } + } +} + +class FileUploadProgressWithoutBar extends FileUploadProgress { + constructor(stateToProgress: StateToProgressWithoutProgressBar, initial_state: string) { + super(stateToProgress, initial_state) + } + + processImportStatus = (): void => { + if (this.continue) { + let continueFunction = checkImportProgress.bind(null, this) + + if (this.currentState.isFinished) { + continueFunction = this.processComplete + } + + setTimeout(continueFunction, this.currentState.wait) + } else { + this.processComplete() + } + } +} diff --git a/app/typescript/shared/checkboxAccordion.ts b/app/typescript/shared/checkboxAccordion.ts new file mode 100644 index 0000000000..f3e8249f3e --- /dev/null +++ b/app/typescript/shared/checkboxAccordion.ts @@ -0,0 +1,246 @@ +interface AccordionItemInterface { + toggleChecked: (isChecked: boolean) => void +} + +interface AccordionNamedItemInterface { + accordionItemDetails: AccordionItemDetails + isChecked: (isChecked: boolean) => void +} + +interface AccordionSectionInterface { + checkAllSelected: () => void + toggleCheckAll: (isAllChecked?: boolean) => void +} + +interface BasketItem { + removeBasketItem: () => void +} + +type BasketInterface = { + addItemToBasket: (accordionItemDetails: AccordionNamedItem) => BasketItem + appendBasketItem: (basketItemHTML: string) => void + removeBasketItem: () => void +} + +type AccordionItemDetails = { + groupID: string + itemID: string + text: string +} + +abstract class AccordionItem implements AccordionItemInterface { + protected $checkBox: JQuery + protected accordionSection: AccordionSection + protected basket: Basket + + constructor(accordionSection: AccordionSection, basket: Basket, $checkBox: JQuery) { + this.accordionSection = accordionSection + this.basket = basket + this.$checkBox = $checkBox + } + + abstract toggleChecked(isChecked: boolean): void + protected abstract setEventListeners(): void +} + +class AccordionNamedItem extends AccordionItem implements AccordionNamedItemInterface { + accordionItemDetails: AccordionItemDetails + private basketItem?: BasketItem + + constructor(accordionSection: AccordionSection, basket: Basket, $checkBox: JQuery) { + super(accordionSection, basket, $checkBox) + this.accordionItemDetails = { + groupID: $checkBox.attr('sectionid') || '', + itemID: $checkBox.attr('id') || '', + text: $checkBox.attr('title') || '' + } + + if (this.isChecked() && this.basketItem === undefined) this.basketItem = this.basket.addItemToBasket(this) + + this.setEventListeners() + } + + toggleChecked = (isChecked: boolean): void => { + if (isChecked) { + if (this.basketItem === undefined) this.basketItem = this.basket.addItemToBasket(this) + } else if (this.basketItem !== undefined) { + this.basketItem.removeBasketItem() + delete this.basketItem + } + + this.$checkBox.prop('checked', isChecked) + this.accordionSection.checkAllSelected() + } + + isChecked = (): boolean => this.$checkBox.is(':checked') + + protected setEventListeners = (): void => { + this.$checkBox.on('click', (): void => this.toggleChecked(this.isChecked())) + } +} + +class AccordionSelectAllItem extends AccordionItem { + constructor(accordionSection: AccordionSection, basket: Basket, $checkBox: JQuery) { + super(accordionSection, basket, $checkBox) + + this.setEventListeners() + } + + protected setEventListeners = (): void => { + this.$checkBox.on('click', () => this.accordionSection.toggleCheckAll()) + } + + toggleChecked = (isChecked: boolean): void => { + this.$checkBox.prop('checked', isChecked) + } +} + +class AccordionSection implements AccordionSectionInterface { + private checkBoxes: Array = [] + private numberOfCheckboxes: number + private selectAllcheckBox: AccordionSelectAllItem + + constructor (basket: Basket, $section: JQuery) { + $section.find('div.govuk-checkboxes__item:not(.ccs-select-all) > input.govuk-checkboxes__input').each((_index: number, checkBox: HTMLElement) => { + this.checkBoxes.push(new AccordionNamedItem(this, basket, $(checkBox) as JQuery)) + }) + + this.numberOfCheckboxes = this.checkBoxes.length + this.selectAllcheckBox = new AccordionSelectAllItem(this, basket, $section.find('div.ccs-select-all > input') as JQuery) + } + + private numberOfCheckedItems = (): number => this.checkBoxes.filter((accordionItem: AccordionNamedItem): boolean => accordionItem.isChecked()).length + + checkAllSelected = (): void => { + this.selectAllcheckBox.toggleChecked(this.numberOfCheckboxes === this.numberOfCheckedItems()) + } + + toggleCheckAll = (isAllChecked?: boolean): void => { + if (isAllChecked === undefined) isAllChecked = this.numberOfCheckboxes > this.numberOfCheckedItems() + const checkAll = isAllChecked + + this.checkBoxes.forEach((checkBox: AccordionNamedItem) => checkBox.toggleChecked(checkAll)) + this.selectAllcheckBox.toggleChecked(checkAll) + } +} + +class BasketItem implements BasketItem { + private basket: Basket + private accordionNamedItem: AccordionNamedItem + private $basketItem: JQuery + private $removeItemLink: JQuery + + constructor(basket: Basket, accordionNamedItem: AccordionNamedItem) { + this.basket = basket + this.accordionNamedItem = accordionNamedItem + basket.appendBasketItem(this.getBasketItemHTML()) + this.$basketItem = $(`#${accordionNamedItem.accordionItemDetails.itemID}_basket`) + this.$removeItemLink = $(`#${accordionNamedItem.accordionItemDetails.itemID}_removeLink`) + this.setEventListener() + } + + private getBasketItemHTML = (): string => { + const removeLink = `` + const itemText = `
    ${this.accordionNamedItem.accordionItemDetails.text}
    ` + + return `
  • ${removeLink}${itemText}
  • ` + } + + removeBasketItem = (): void => { + this.$basketItem.remove() + + this.basket.removeBasketItem() + } + + private setEventListener = (): void => { + this.$removeItemLink.on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + this.accordionNamedItem.toggleChecked(false) + }) + } +} + +class Basket implements BasketInterface { + private $basket: JQuery = $('.basket') + private $itemList: JQuery = this.$basket.find('ul') + private $numberOfItems: JQuery = this.$basket.find('h3') + private $removeAllLink: JQuery = this.$basket.find('div > a') as JQuery + private accordionSections: Array = [] + + private textOptions: {[key: string]: string} = { + no_items: this.$numberOfItems.data('txt02'), + single_item: this.$numberOfItems.data('txt03'), + plural_items: this.$numberOfItems.data('txt01') + } + + constructor() { + $('.govuk-accordion__section.chooser-section').each((_index: number, accordionSection: HTMLElement) => { + this.accordionSections.push(new AccordionSection(this, $(accordionSection))) + }) + + this.updateNumberOfItems() + + this.setEventListeners() + } + + private numberOfItems = (): number => this.$itemList.find('li').length + + addItemToBasket = (accordionNamedItem: AccordionNamedItem): BasketItem => { + const basketItem = new BasketItem(this, accordionNamedItem) + + this.updateNumberOfItems() + + return basketItem + } + + appendBasketItem = (basketItemHTML: string): void => { + this.$itemList.append(basketItemHTML) + } + + removeBasketItem = (): void => this.updateNumberOfItems() + + private updateNumberOfItems = (): void => { + const numberOfItems: number = this.numberOfItems() + + let numberOfItemsNumber = String(numberOfItems) + let numberOfItemsText + let isShown = true + + if (numberOfItems == 0) { + numberOfItemsNumber = '' + numberOfItemsText = this.textOptions.no_items + isShown = false + } else if (numberOfItems == 1) { + numberOfItemsText = this.textOptions.single_item + isShown = false + } else { + numberOfItemsText = this.textOptions.plural_items + } + + this.$numberOfItems.html(`${numberOfItemsNumber} ${numberOfItemsText}`) + this.toggleRemoveAllButton(isShown) + } + + private toggleRemoveAllButton = (isShown: boolean): void => { + isShown ? this.$removeAllLink.show() : this.$removeAllLink.hide() + } + + private removeAll = (event: JQuery.ClickEvent): void => { + event.preventDefault() + + Object.values(this.accordionSections).forEach((accordionSection) => accordionSection.toggleCheckAll(false)) + + this.updateNumberOfItems() + } + + private setEventListeners = (): void => { + this.$removeAllLink.on('click', (event: JQuery.ClickEvent) => this.removeAll(event)) + } +} + +const initCheckboxAccordion = (): void => { + if ($('.govuk-accordion__section.chooser-section').length > 0) new Basket() +} + +$(() => initCheckboxAccordion()) diff --git a/app/typescript/shared/cookieBanner.ts b/app/typescript/shared/cookieBanner.ts new file mode 100644 index 0000000000..d6393a9a03 --- /dev/null +++ b/app/typescript/shared/cookieBanner.ts @@ -0,0 +1,80 @@ +type CookieBannerFormData = { + [key: string]: string +} + +const removeGACookies = (cookieBannerFormData: CookieBannerFormData, successFunction: () => void) => { + let success = false + + $.ajax({ + type: 'PUT', + url: '/api/v2/update-cookie-settings', + data: cookieBannerFormData, + dataType: 'json', + success() { + success = true + }, + complete() { + if (success) successFunction() + }, + }) +} + +const cookiesSaved = () => { + const $cookieSettingsSaved: JQuery = $('#cookie-settings-saved') + + $cookieSettingsSaved.show() + + const offsetCoordinates = $cookieSettingsSaved.offset() + + if (offsetCoordinates !== undefined) { + $('html, body').animate({ scrollTop: offsetCoordinates.top }, 'slow') + } +} + +const cookieSettingsViewed = ($newBanner: JQuery) => { + $('#cookie-options-container').hide() + $newBanner.show() +} + +const updateBanner = (isAccepeted: string, $newBanner: JQuery) => { + removeGACookies( + { + ga_cookie_usage: isAccepeted, + glassbox_cookie_usage: isAccepeted, + }, + cookieSettingsViewed.bind(null, $newBanner), + ) +} + +const initCookieBanner = (): void => { + const obsoleteCookies: string[] = ['crown_marketplace_cookie_settings_viewed', 'crown_marketplace_google_analytics_enabled'] + + obsoleteCookies.forEach((cookieName: string) => { + if (document.cookie.includes(`${cookieName}=`)) document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` + }) + + $('[name="cookies"]').on('click', (event: JQuery.ClickEvent) => { + event.preventDefault() + + const buttonValue: string = event.currentTarget.value + + updateBanner(String(buttonValue === 'accept'), $(`#cookies-${buttonValue}ed-container`)) + }) + + const $form: JQuery = $('#update-cookie-setings') + + $form.on('submit', (event: JQuery.SubmitEvent) => { + event.preventDefault() + + $('#cookie-settings-saved').show() + + const formData = Object.fromEntries($form.serializeArray().map((element) => [element.name, element.value])) + + removeGACookies( + formData, + cookiesSaved, + ) + }) +} + +$(() => initCookieBanner()) diff --git a/app/typescript/shared/googleAnalytics.ts b/app/typescript/shared/googleAnalytics.ts new file mode 100644 index 0000000000..c8390706ef --- /dev/null +++ b/app/typescript/shared/googleAnalytics.ts @@ -0,0 +1,39 @@ +interface Window { + gaTrackingId: string +} + +declare function gtag(config: string, gaTrackingId: string, params: {[key: string]: string|boolean}): void + +const cssClassToClickPath: {[key: string]: string} = { + 'ga-crown-logo': 'crown_logo', + 'ga-feedback-mailto': 'feedback', + 'ga-support-mailto': 'support', + 'ga-auth-cognito': 'cognito_login', + 'ga-auth-dfe': 'dfe_login', + 'ga-print-link': 'print', + 'ga-download-shortlist': 'shortlist_download', + 'ga-download-calculator': 'calculator_download' +} + +const initGoogleAnalytics = (): void => { + if (window.gaTrackingId) { + for (const cssClass in cssClassToClickPath) { + const $elements: JQuery = $(`.${cssClass}`) + + $elements.each((_index: number, $element: HTMLElement) => { + $($element).on('click', () => { + const page: string = cssClassToClickPath[cssClass] + const params: {[key: string]: string|boolean} = { + anonymize_ip: true, + page_title: page, + page_path: `/external/${page}` + } + + gtag('config', window.gaTrackingId, params) + }) + }) + } + } +} + +$(() => initGoogleAnalytics()) \ No newline at end of file diff --git a/app/typescript/shared/govukFrontend.ts b/app/typescript/shared/govukFrontend.ts new file mode 100644 index 0000000000..116f0fab40 --- /dev/null +++ b/app/typescript/shared/govukFrontend.ts @@ -0,0 +1,9 @@ +interface Window { + GOVUKFrontend: { + initAll(): void + } +} + +$(() => { + window.GOVUKFrontend.initAll() +}) diff --git a/app/typescript/shared/passwordStrength.ts b/app/typescript/shared/passwordStrength.ts new file mode 100644 index 0000000000..501d03a917 --- /dev/null +++ b/app/typescript/shared/passwordStrength.ts @@ -0,0 +1,54 @@ +const cReg = (): RegExp => { + return new RegExp('^.{8,}') +} + +const pReg = (): RegExp => { + return new RegExp('^(?=.*?[#?!@£$%^&*-])') +} + +const uReg = (): RegExp => { + return new RegExp('^(?=.*?[A-Z])') +} + +const numReg = (): RegExp => { + return new RegExp('^(?=.*[0-9])') +} + +const theTests: Array<[RegExp, JQuery]> = [ + [cReg(), $('#passeight')], + [pReg(), $('#passsymbol')], + [uReg(), $('#passcap')], + [numReg(), $('#passnum')], +] + +const runTests = ($input: JQuery): void => { + const input_text = String($input.val()) + + theTests.forEach((test) => { + if (test[0].test(input_text)) { + test[1].removeClass('wrong').addClass('correct') + } else { + test[1].removeClass('correct').addClass('wrong') + } + }) +} + +const passwordStrength = ($input: JQuery): void => { + $input.on('keyup', () => runTests($input)) +} + +const passwordStrengthInit = (): void => { + const form: JQuery = $('#main-content form.ccs-form') + + if (form.length) { + const formIDs: string[] = ['cop_sign_in_form', 'cop_change_password_form', 'cop_register', 'cop_confirmation_code', 'cog_forgot_password_request_form', 'cog_forgot_password_reset_form'] + + formIDs.forEach((formID) => { + if (form.is(`#${formID}`)) { + passwordStrength($('#password01')) + } + }) + } +} + +$(() => passwordStrengthInit()) diff --git a/app/typescript/shared/stepByStepNav.ts b/app/typescript/shared/stepByStepNav.ts new file mode 100644 index 0000000000..0fe3680743 --- /dev/null +++ b/app/typescript/shared/stepByStepNav.ts @@ -0,0 +1,229 @@ +interface GOVUKStepByStepNavStepSectionInterface { + toggleSection(isShown: boolean): void +} + +interface GOVUKStepByStepNavStepInterface { + toggleOnClick: (event: JQuery.ClickEvent) => void + isShown: () => boolean + toggleSection: (isShown: boolean) => void +} + +interface GOVUKStepByStepNavInterface { + prependAndCreateShowHideAllButton: (showHideAllButtonHTML: string) => JQuery + toggleAllOnClick: (event: JQuery.ClickEvent) => void + checkIfAllShown: () => void +} + +class GOVUKStepByStepNavShowHideAllButton { + private govukStepByStepNav: GOVUKStepByStepNav + private showAllText: string + private hideAllText: string + private $showHideAllButton: JQuery + private $showHideAllButtonnChevron: JQuery + private $showHideAllButtonTextSpan: JQuery + + constructor(govukStepByStepNav: GOVUKStepByStepNav, showAllText: string, hideAllText: string) { + this.govukStepByStepNav = govukStepByStepNav + this.showAllText = showAllText + this.hideAllText = hideAllText + + this.$showHideAllButton = govukStepByStepNav.prependAndCreateShowHideAllButton(this.generateButtonHTML()) + this.$showHideAllButtonnChevron = this.$showHideAllButton.find('.gem-c-step-nav__chevron') + this.$showHideAllButtonTextSpan = this.$showHideAllButton.find('.gem-c-step-nav__button-text') + + this.setEventListener() + } + + private generateButtonHTML = (): string => { + return ` + + ` + } + + private setEventListener = (): void => { + this.$showHideAllButton.on('click', this.govukStepByStepNav.toggleAllOnClick.bind(this)) + } + + toggleShowHideAll = (showAll: boolean): void => { + if(showAll) { + this.$showHideAllButtonnChevron.removeClass('gem-c-step-nav__chevron--down') + this.$showHideAllButtonTextSpan.text(this.hideAllText) + } else { + this.$showHideAllButtonnChevron.addClass('gem-c-step-nav__chevron--down') + this.$showHideAllButtonTextSpan.text(this.showAllText) + } + + this.$showHideAllButton.attr('aria-expanded', String(showAll)) + } +} + +class GOVUKStepByStepNavStepShowHideButton implements GOVUKStepByStepNavStepSectionInterface { + private govukStepByStepNavStep: GOVUKStepByStepNavStep + private showText: string + private hideText: string + private $button: JQuery + private $buttonChevron: JQuery + private $buttonTextSpan: JQuery + + constructor (govukStepByStepNavStep: GOVUKStepByStepNavStep, showText: string, hideText: string, $titleSection: JQuery, controlledPanel: string) { + this.govukStepByStepNavStep = govukStepByStepNavStep + this.showText = showText + this.hideText = hideText + + const titleText: string = $titleSection.find('.js-step-title-text').text() + + $titleSection.html(this.generateButtonHTML(titleText, controlledPanel)) + + this.$button = $titleSection.find('button') + this.$buttonChevron = this.$button.find('.gem-c-step-nav__chevron') + this.$buttonTextSpan = this.$button.find('.gem-c-step-nav__button-text') + + this.setEventListener() + } + + private generateButtonHTML = (titleText: string, controlledPanel: string): string => { + return ` +