cityssm/parking-ticket-system

View on GitHub
public/javascripts/offence-maint.ts

Summary

Maintainability
D
2 days
Test Coverage
/* eslint-disable unicorn/filename-case, unicorn/prefer-module, eslint-comments/disable-enable-pair */
/* eslint-disable @typescript-eslint/indent */

import type { BulmaJS } from '@cityssm/bulma-js/types.js'
import type { cityssmGlobal } from '@cityssm/bulma-webapp-js/src/types.js'

import type { ptsGlobal } from '../../types/publicTypes.js'
import type {
  ParkingBylaw,
  ParkingLocation,
  ParkingOffence
} from '../../types/recordTypes.js'

declare const cityssm: cityssmGlobal
declare const bulmaJS: BulmaJS
declare const pts: ptsGlobal

type UpdateOffenceResponseJSON =
  | {
      success: true
      offences: ParkingOffence[]
      message?: string
    }
  | {
      success: false
      message?: string
    }

// eslint-disable-next-line sonarjs/cognitive-complexity
;(() => {
  const offenceMap = new Map<string, ParkingOffence>()

  const offenceAccountNumberPatternString =
    exports.accountNumberPattern as string
  delete exports.accountNumberPattern

  const locationMap = new Map<string, ParkingLocation>()
  const limitResultsCheckboxElement = document.querySelector(
    '#offenceFilter--limitResults'
  ) as HTMLInputElement
  const resultsElement = document.querySelector(
    '#offenceResults'
  ) as HTMLElement

  const locationInputElement = document.querySelector(
    '#offenceFilter--location'
  ) as HTMLInputElement

  const locationTextElement = document.querySelector(
    '#offenceFilter--locationText'
  ) as HTMLElement

  let locationKeyFilterIsSet = false
  let locationKeyFilter = ''

  const bylawMap = new Map<string, ParkingBylaw>()

  const bylawInputElement = document.querySelector(
    '#offenceFilter--bylaw'
  ) as HTMLInputElement
  const bylawTextElement = document.querySelector(
    '#offenceFilter--bylawText'
  ) as HTMLElement

  let bylawNumberFilterIsSet = false
  let bylawNumberFilter = ''

  function getOffenceMapKey(bylawNumber: string, locationKey: string): string {
    return `${bylawNumber}::::${locationKey}`
  }

  function loadOffenceMap(offenceList: ParkingOffence[]): void {
    offenceMap.clear()

    for (const offence of offenceList) {
      const offenceMapKey = getOffenceMapKey(
        offence.bylawNumber,
        offence.locationKey
      )
      offenceMap.set(offenceMapKey, offence)
    }
  }

  function openEditOffenceModal(clickEvent: Event): void {
    clickEvent.preventDefault()

    const buttonElement = clickEvent.currentTarget as HTMLButtonElement

    const offenceMapKey = getOffenceMapKey(
      buttonElement.dataset.bylawNumber ?? '',
      buttonElement.dataset.locationKey ?? ''
    )

    const offence = offenceMap.get(offenceMapKey) as ParkingOffence
    const location = locationMap.get(offence.locationKey) as ParkingLocation
    const bylaw = bylawMap.get(offence.bylawNumber) as ParkingBylaw

    let editOffenceModalCloseFunction: () => void

    function doDelete(): void {
      cityssm.postJSON(
        `${pts.urlPrefix}/admin/doDeleteOffence`,
        {
          bylawNumber: offence.bylawNumber,
          locationKey: offence.locationKey
        },
        (rawResponseJSON) => {
          const responseJSON = rawResponseJSON as UpdateOffenceResponseJSON

          if (responseJSON.success) {
            loadOffenceMap(responseJSON.offences)
            editOffenceModalCloseFunction()
            renderOffences()
          }
        }
      )
    }

    function confirmDelete(deleteClickEvent: Event): void {
      deleteClickEvent.preventDefault()

      bulmaJS.confirm({
        title: 'Remove Offence',
        message: 'Are you sure you want to remove this offence?',
        contextualColorName: 'warning',
        okButton: {
          text: 'Yes, Remove Offence',
          callbackFunction: doDelete
        }
      })
    }

    function doSubmit(formEvent: Event): void {
      formEvent.preventDefault()

      cityssm.postJSON(
        `${pts.urlPrefix}/admin/doUpdateOffence`,
        formEvent.currentTarget,
        (rawResponseJSON) => {
          const responseJSON = rawResponseJSON as UpdateOffenceResponseJSON
          if (responseJSON.success) {
            loadOffenceMap(responseJSON.offences)
            editOffenceModalCloseFunction()
            renderOffences()
          }
        }
      )
    }

    cityssm.openHtmlModal('offence-edit', {
      onshow() {
        ;(
          document.querySelector(
            '#offenceEdit--locationKey'
          ) as HTMLInputElement
        ).value = offence.locationKey
        ;(
          document.querySelector(
            '#offenceEdit--bylawNumber'
          ) as HTMLInputElement
        ).value = offence.bylawNumber
        ;(
          document.querySelector(
            '#offenceEdit--locationName'
          ) as HTMLSpanElement
        ).textContent = location.locationName
        ;(
          document.querySelector(
            '#offenceEdit--locationClass'
          ) as HTMLSpanElement
        ).textContent = pts.getLocationClass(
          location.locationClassKey
        ).locationClass
        ;(
          document.querySelector('#offenceEdit--bylawNumberSpan') as HTMLElement
        ).textContent = bylaw?.bylawNumber ?? ''
        ;(
          document.querySelector(
            '#offenceEdit--bylawDescription'
          ) as HTMLElement
        ).textContent = bylaw?.bylawDescription ?? ''
        ;(
          document.querySelector(
            '#offenceEdit--parkingOffence'
          ) as HTMLInputElement
        ).value = offence.parkingOffence
        ;(
          document.querySelector(
            '#offenceEdit--offenceAmount'
          ) as HTMLInputElement
        ).value = offence.offenceAmount.toFixed(2)
        ;(
          document.querySelector(
            '#offenceEdit--discountOffenceAmount'
          ) as HTMLInputElement
        ).value = offence.discountOffenceAmount.toFixed(2)
        ;(
          document.querySelector(
            '#offenceEdit--discountDays'
          ) as HTMLInputElement
        ).value = offence.discountDays.toString()

        const accountNumberElement = document.querySelector(
          '#offenceEdit--accountNumber'
        ) as HTMLInputElement
        accountNumberElement.value = offence.accountNumber
        accountNumberElement.setAttribute(
          'pattern',
          offenceAccountNumberPatternString
        )
      },
      onshown(modalElement, closeModalFunction) {
        bulmaJS.toggleHtmlClipped()
        editOffenceModalCloseFunction = closeModalFunction
        ;(
          modalElement.querySelector(
            '#offenceEdit--accountNumber'
          ) as HTMLInputElement
        ).focus()

        modalElement.querySelector('form')?.addEventListener('submit', doSubmit)

        modalElement
          .querySelector('.is-delete-button')
          ?.addEventListener('click', confirmDelete)
      },
      onhidden() {
        bulmaJS.toggleHtmlClipped()
      }
    })
  }

  function addOffence(
    bylawNumber: string,
    locationKey: string,
    returnAndRenderOffences: boolean,
    callbackFunction?: (responseJSON: UpdateOffenceResponseJSON) => void
  ): void {
    cityssm.postJSON(
      `${pts.urlPrefix}/admin/doAddOffence`,
      {
        bylawNumber,
        locationKey,
        returnOffences: returnAndRenderOffences
      },
      (rawResponseJSON) => {
        const responseJSON = rawResponseJSON as UpdateOffenceResponseJSON

        if (responseJSON.success && returnAndRenderOffences) {
          loadOffenceMap(responseJSON.offences)
          renderOffences()
        }

        if (callbackFunction !== undefined) {
          callbackFunction(responseJSON)
        }
      }
    )
  }

  function openAddOffenceFromListModal(): void {
    let doRefreshOnClose = false

    function addFunction(clickEvent: Event): void {
      clickEvent.preventDefault()

      const linkElement = clickEvent.currentTarget as HTMLAnchorElement

      const bylawNumber = linkElement.dataset.bylawNumber ?? ''
      const locationKey = linkElement.dataset.locationKey ?? ''

      addOffence(bylawNumber, locationKey, false, (responseJSON) => {
        if (responseJSON.success) {
          linkElement.remove()
          doRefreshOnClose = true
        }
      })
    }

    cityssm.openHtmlModal('offence-addFromList', {
      onshow(modalElement) {
        let titleHTML = ''
        let selectedHTML = ''

        if (locationKeyFilterIsSet) {
          titleHTML = 'Select By-Laws'

          const location = locationMap.get(locationKeyFilter) as ParkingLocation
          const locationClass = pts.getLocationClass(location.locationClassKey)

          selectedHTML = `${cityssm.escapeHTML(location.locationName)}<br />
            <span class="is-size-7">
            ${cityssm.escapeHTML(locationClass.locationClass)}
            </span>`
        } else {
          titleHTML = 'Select Locations'

          const bylaw = bylawMap.get(bylawNumberFilter) as ParkingBylaw

          selectedHTML = `${cityssm.escapeHTML(bylaw.bylawNumber)}<br />
            <span class="is-size-7">
            ${bylaw.bylawDescription}
            </span>`
        }

        // eslint-disable-next-line no-unsanitized/property
        ;(
          modalElement.querySelector('.modal-card-title') as HTMLElement
        ).innerHTML = titleHTML

        // eslint-disable-next-line no-unsanitized/property
        ;(
          document.querySelector('#addContainer--selected') as HTMLElement
        ).innerHTML = selectedHTML
      },
      onshown() {
        const listElement = document.createElement('div')
        listElement.className = 'panel'

        let displayCount = 0

        if (locationKeyFilterIsSet) {
          for (const bylaw of bylawMap.values()) {
            const offenceMapKey = getOffenceMapKey(
              bylaw.bylawNumber,
              locationKeyFilter
            )

            if (offenceMap.has(offenceMapKey)) {
              continue
            }

            displayCount += 1

            const linkElement = document.createElement('a')
            linkElement.className = 'panel-block is-block'
            linkElement.dataset.bylawNumber = bylaw.bylawNumber
            linkElement.dataset.locationKey = locationKeyFilter

            linkElement.innerHTML = `${cityssm.escapeHTML(
              bylaw.bylawNumber
            )}<br />
              <span class="is-size-7">
              ${cityssm.escapeHTML(bylaw.bylawDescription)}
              </span>`

            linkElement.addEventListener('click', addFunction)

            listElement.append(linkElement)
          }
        } else {
          for (const location of locationMap.values()) {
            const offenceMapKey = getOffenceMapKey(
              bylawNumberFilter,
              location.locationKey
            )

            if (offenceMap.has(offenceMapKey)) {
              continue
            }

            displayCount += 1

            const linkElement = document.createElement('a')
            linkElement.className = 'panel-block is-block'
            linkElement.dataset.bylawNumber = bylawNumberFilter
            linkElement.dataset.locationKey = location.locationKey

            linkElement.innerHTML = `${cityssm.escapeHTML(
              location.locationName
            )}<br />
              <span class="is-size-7">
              ${cityssm.escapeHTML(
                pts.getLocationClass(location.locationClassKey).locationClass
              )}
              </span>`

            linkElement.addEventListener('click', addFunction)

            listElement.append(linkElement)
          }
        }

        const addResultsElement = document.querySelector(
          '#addContainer--results'
        ) as HTMLElement
        cityssm.clearElement(addResultsElement)

        if (displayCount === 0) {
          addResultsElement.innerHTML = `<div class="message is-info">
            <div class="message-body">There are no offence records available for creation.</div>
            </div>`
        } else {
          addResultsElement.append(listElement)
        }
      },
      onremoved() {
        if (doRefreshOnClose) {
          cityssm.postJSON(
            `${pts.urlPrefix}/offences/doGetAllOffences`,
            {},
            (rawResponseJSON) => {
              const offenceList = rawResponseJSON as unknown as ParkingOffence[]
              loadOffenceMap(offenceList)
              renderOffences()
            }
          )
        }
      }
    })
  }

  function renderOffences(): void {
    const tbodyElement = document.createElement('tbody')

    let matchCount = 0

    const displayLimit = limitResultsCheckboxElement.checked
      ? Number.parseInt(limitResultsCheckboxElement.value, 10)
      : offenceMap.size

    for (const offence of offenceMap.values()) {
      if (matchCount >= displayLimit) {
        continue
      }

      // Ensure offence matches filters
      if (
        (locationKeyFilterIsSet && locationKeyFilter !== offence.locationKey) ||
        (bylawNumberFilterIsSet && bylawNumberFilter !== offence.bylawNumber)
      ) {
        continue
      }

      // Ensure location record exists
      const location = locationMap.get(offence.locationKey)

      if (location === undefined) {
        continue
      }

      // Ensure by-law record exists
      const bylaw = bylawMap.get(offence.bylawNumber)

      if (bylaw === undefined) {
        continue
      }

      matchCount += 1

      if (matchCount > displayLimit) {
        continue
      }

      const trElement = document.createElement('tr')

      // eslint-disable-next-line no-unsanitized/property
      trElement.innerHTML = `<td class="has-border-right-width-2">
          ${cityssm.escapeHTML(location.locationName)}<br />
          <span class="is-size-7">
            ${cityssm.escapeHTML(
              pts.getLocationClass(location.locationClassKey).locationClass
            )}
          </span>
        </td>
        <td class="has-border-right-width-2">
          <strong>${cityssm.escapeHTML(bylaw.bylawNumber)}</strong><br />
          <span class="is-size-7">
            ${cityssm.escapeHTML(bylaw.bylawDescription)}
          </span>
        </td>
        <td class="has-text-right has-tooltip-bottom" data-tooltip="Set Rate">
          $${cityssm.escapeHTML(offence.offenceAmount.toFixed(2))}<br />
          <span class="is-size-7">
            ${cityssm.escapeHTML(offence.accountNumber)}
          </span>
        </td>
        <td class="has-text-right has-tooltip-bottom" data-tooltip="Discount Rate">
          $${cityssm.escapeHTML(offence.discountOffenceAmount.toFixed(2))}<br />
          <span class="is-size-7">
          ${cityssm.escapeHTML(offence.discountDays.toString())}
          day${offence.discountDays === 1 ? '' : 's'}
          </span>
        </td>
        <td class="has-border-right-width-2">
          <div class="is-size-7">
            ${cityssm.escapeHTML(offence.parkingOffence)}
          </div>
        </td>
        <td class="has-text-right">
          <button class="button is-small"
            data-bylaw-number="${cityssm.escapeHTML(offence.bylawNumber)}"
            data-location-key="${cityssm.escapeHTML(
              offence.locationKey
            )}" type="button">
            <span class="icon is-small"><i class="fas fa-pencil-alt" aria-hidden="true"></i></span>
            <span>Edit</span>
          </button>
        </td>`

      trElement
        .querySelector('button')
        ?.addEventListener('click', openEditOffenceModal)

      tbodyElement.append(trElement)
    }

    cityssm.clearElement(resultsElement)

    if (matchCount === 0) {
      resultsElement.innerHTML = `<div class="message is-info">
        <div class="message-body">
        <p>There are no offences that match the given criteria.</p>
        </div>
        </div>`

      return
    }

    resultsElement.innerHTML = `<table class="table is-striped is-hoverable is-fullwidth">
      <thead><tr>
        <th class="has-border-right-width-2">Location</th>
        <th class="has-border-right-width-2">By-Law</th>
        <th class="has-border-right-width-2" colspan="3">Offence</th>
        <th class="has-width-50"></th>
      </tr></thead>
      </table>`

    resultsElement.querySelector('table')?.append(tbodyElement)

    if (matchCount > displayLimit) {
      resultsElement.insertAdjacentHTML(
        'afterbegin',
        `<div class="message is-warning">
          <div class="message-body has-text-centered">Limit Reached</div>
          </div>`
      )
    }
  }

  document
    .querySelector('#is-add-offence-button')
    ?.addEventListener('click', (clickEvent) => {
      clickEvent.preventDefault()

      if (locationKeyFilterIsSet && bylawNumberFilterIsSet) {
        const bylaw = bylawMap.get(bylawNumberFilter) as ParkingBylaw
        const location = locationMap.get(locationKeyFilter) as ParkingLocation

        bulmaJS.confirm({
          title: 'Create Offence?',
          message: `<p class="has-text-centered">Are you sure you want to create the offence record below?</p>
            <div class="columns my-4">
            <div class="column has-text-centered">
              ${cityssm.escapeHTML(location.locationName)}<br />
              <span class="is-size-7">
              ${cityssm.escapeHTML(
                pts.getLocationClass(location.locationClassKey).locationClass
              )}
              </span>
            </div>
            <div class="column has-text-centered">
              ${cityssm.escapeHTML(bylaw.bylawNumber)}<br />
              <span class="is-size-7">
                ${cityssm.escapeHTML(bylaw.bylawDescription)}
              </span>
            </div>
            </div>`,
          messageIsHtml: true,
          contextualColorName: 'info',
          okButton: {
            text: 'Yes, Create Offence',
            callbackFunction: () => {
              addOffence(
                bylawNumberFilter,
                locationKeyFilter,
                true,
                (responseJSON) => {
                  if (!responseJSON.success) {
                    bulmaJS.alert({
                      title: 'Offence Not Added',
                      message: responseJSON.message ?? '',
                      contextualColorName: 'danger'
                    })
                  } else if ((responseJSON.message ?? '') !== '') {
                    bulmaJS.alert({
                      title: 'Offence Added Successfully',
                      message: responseJSON.message ?? '',
                      contextualColorName: 'warning'
                    })
                  }
                }
              )
            }
          }
        })
      } else if (locationKeyFilterIsSet || bylawNumberFilterIsSet) {
        openAddOffenceFromListModal()
      } else {
        bulmaJS.alert({
          title: 'How to Create a New Offence',
          message:
            'To add an offence, use the main filters to select either a location, a by-law, or both.',
          contextualColorName: 'warning'
        })
      }
    })

  // Location filter setup

  const selectLocationFilterButtonElement = document.querySelector(
    '#is-select-location-filter-button'
  ) as HTMLButtonElement

  function clearLocationFilter(): void {
    locationInputElement.value = ''
    cityssm.clearElement(locationTextElement)

    locationKeyFilter = ''
    locationKeyFilterIsSet = false
  }

  function openSelectLocationFilterModal(): void {
    let selectLocationCloseModalFunction: () => void

    function doSelect(clickEvent: Event): void {
      clickEvent.preventDefault()

      const location = locationMap.get(
        (clickEvent.currentTarget as HTMLAnchorElement).dataset.locationKey ??
          ''
      ) as ParkingLocation

      locationKeyFilterIsSet = true
      locationKeyFilter = location.locationKey

      locationInputElement.value = location.locationName

      selectLocationCloseModalFunction()

      renderOffences()
    }

    cityssm.openHtmlModal('location-select', {
      onshow() {
        const listElement = document.createElement('div')
        listElement.className = 'panel mb-4'

        for (const location of locationMap.values()) {
          const linkElement = document.createElement('a')

          linkElement.className = 'panel-block is-block'
          linkElement.dataset.locationKey = location.locationKey
          linkElement.setAttribute('href', '#')

          linkElement.innerHTML = `<div class="level">
            <div class="level-left">
              ${cityssm.escapeHTML(location.locationName)}
            </div>
            <div class="level-right">
              ${cityssm.escapeHTML(
                pts.getLocationClass(location.locationClassKey).locationClass
              )}
            </div>
            </div>`

          linkElement.addEventListener('click', doSelect)

          listElement.append(linkElement)
        }

        const listContainerElement = document.querySelector(
          '#container--parkingLocations'
        ) as HTMLElement
        cityssm.clearElement(listContainerElement)
        listContainerElement.append(listElement)
      },
      onshown(_modalElement, closeModalFunction) {
        bulmaJS.toggleHtmlClipped()
        selectLocationCloseModalFunction = closeModalFunction
        ;(
          document.querySelector(
            '#container--parkingLocations a'
          ) as HTMLElement | null
        )?.focus()
      },
      onremoved() {
        bulmaJS.toggleHtmlClipped()
        selectLocationFilterButtonElement.focus()
      }
    })
  }

  locationInputElement.addEventListener(
    'dblclick',
    openSelectLocationFilterModal
  )

  selectLocationFilterButtonElement.addEventListener(
    'click',
    openSelectLocationFilterModal
  )

  document
    .querySelector('#is-clear-location-filter-button')
    ?.addEventListener('click', () => {
      clearLocationFilter()
      renderOffences()
    })

  if (!locationKeyFilterIsSet) {
    clearLocationFilter()
  }

  // By-law filter setup

  const selectBylawFilterButtoneElement = document.querySelector(
    '#is-select-bylaw-filter-button'
  ) as HTMLButtonElement

  function clearBylawFilter(): void {
    bylawInputElement.value = ''
    cityssm.clearElement(bylawTextElement)

    bylawNumberFilter = ''
    bylawNumberFilterIsSet = false
  }

  function openSelectBylawFilterModal(): void {
    let selectBylawCloseModalFunction: () => void

    function doSelect(clickEvent: Event): void {
      clickEvent.preventDefault()

      const bylaw = bylawMap.get(
        (clickEvent.currentTarget as HTMLAnchorElement).dataset.bylawNumber ??
          ''
      ) as ParkingBylaw

      bylawNumberFilterIsSet = true
      bylawNumberFilter = bylaw.bylawNumber

      bylawInputElement.value = bylaw.bylawNumber
      bylawTextElement.textContent = bylaw.bylawDescription

      selectBylawCloseModalFunction()

      renderOffences()
    }

    cityssm.openHtmlModal('bylaw-select', {
      onshow() {
        const listElement = document.createElement('div')
        listElement.className = 'panel mb-4'

        for (const bylaw of bylawMap.values()) {
          const linkElement = document.createElement('a')

          linkElement.className = 'panel-block is-block'
          linkElement.dataset.bylawNumber = bylaw.bylawNumber
          linkElement.setAttribute('href', '#')

          linkElement.innerHTML = `${cityssm.escapeHTML(
            bylaw.bylawNumber
          )}<br />
            <span class="is-size-7">
            ${cityssm.escapeHTML(bylaw.bylawDescription)}
            </span>`

          linkElement.addEventListener('click', doSelect)

          listElement.append(linkElement)
        }

        const listContainerElement = document.querySelector(
          '#container--parkingBylaws'
        ) as HTMLElement
        cityssm.clearElement(listContainerElement)
        listContainerElement.append(listElement)
      },
      onshown(_modalElement, closeModalFunction) {
        bulmaJS.toggleHtmlClipped()
        selectBylawCloseModalFunction = closeModalFunction
        ;(
          document.querySelector(
            '#container--parkingBylaws a'
          ) as HTMLElement | null
        )?.focus()
      },
      onremoved() {
        bulmaJS.toggleHtmlClipped()
        selectBylawFilterButtoneElement.focus()
      }
    })
  }

  bylawInputElement.addEventListener('dblclick', openSelectBylawFilterModal)

  selectBylawFilterButtoneElement.addEventListener(
    'click',
    openSelectBylawFilterModal
  )

  document
    .querySelector('#is-clear-bylaw-filter-button')
    ?.addEventListener('click', () => {
      clearBylawFilter()
      renderOffences()
    })

  if (!bylawNumberFilterIsSet) {
    clearBylawFilter()
  }

  // Limit checkbox setup

  limitResultsCheckboxElement.addEventListener('change', renderOffences)

  // Load locationMap

  for (const location of exports.locations as ParkingLocation[]) {
    locationMap.set(location.locationKey, location)
  }

  delete exports.locations

  // Load bylawMap

  for (const bylaw of exports.bylaws as ParkingBylaw[]) {
    bylawMap.set(bylaw.bylawNumber, bylaw)
  }

  delete exports.bylaws

  // Load offenceList

  loadOffenceMap(exports.offences as ParkingOffence[])
  delete exports.offences

  // Load locationClasses

  pts.getDefaultConfigProperty('locationClasses', renderOffences)
})()