cityssm/parking-ticket-system

View on GitHub
public/javascripts/ticket-statuses-edit.ts

Summary

Maintainability
F
6 days
Test Coverage
/* eslint-disable unicorn/filename-case, unicorn/prefer-module, eslint-comments/disable-enable-pair */
/* eslint-disable no-extra-semi */

// eslint-disable-next-line n/no-missing-import
import type { BulmaJS } from '@cityssm/bulma-js/types.js'
import type { cityssmGlobal } from '@cityssm/bulma-webapp-js/src/types.js'

import type { ConfigParkingTicketStatus } from '../../types/configTypes.js'
import type { ptsGlobal } from '../../types/publicTypes.js'
import type { ParkingTicketStatusLog } from '../../types/recordTypes.js'

declare const bulmaJS: BulmaJS
declare const cityssm: cityssmGlobal
declare const pts: ptsGlobal
;(() => {
  const ticketId = cityssm.escapeHTML(
    (document.querySelector('#ticket--ticketId') as HTMLInputElement).value
  )

  const statusPanelElement = document.querySelector(
    '#is-status-panel'
  ) as HTMLElement

  let statusList = exports.ticketStatusLog as ParkingTicketStatusLog[]
  delete exports.ticketStatusLog

  function clearStatusPanelFunction(): void {
    const panelBlockElements =
      statusPanelElement.querySelectorAll('.panel-block')

    for (const panelBlockElement of panelBlockElements) {
      panelBlockElement.remove()
    }
  }

  function doResolve(): void {
    cityssm.postJSON(
      `${pts.urlPrefix}/tickets/doResolveTicket`,
      {
        ticketId
      },
      (rawResponseJSON) => {
        const responseJSON = rawResponseJSON as { success: boolean }
        if (responseJSON.success) {
          window.location.href = `${pts.urlPrefix}/tickets/${ticketId}`
        }
      }
    )
  }

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

    bulmaJS.confirm({
      title: 'Mark Ticket as Resolved',
      message:
        'Once resolved, you will no longer be able to make changes to the ticket.',
      contextualColorName: 'info',
      okButton: {
        text: 'Yes, Resolve Ticket',
        callbackFunction: doResolve
      }
    })
  }

  function confirmDeleteStatusFunction(clickEvent: Event): void {
    const statusIndex = (clickEvent.currentTarget as HTMLElement).dataset
      .statusIndex

    function doDeleteStatus(): void {
      cityssm.postJSON(
        `${pts.urlPrefix}/tickets/doDeleteStatus`,
        {
          ticketId,
          statusIndex
        },
        (rawResponseJSON) => {
          const responseJSON = rawResponseJSON as { success: boolean }
          if (responseJSON.success) {
            getStatusesFunction()
          }
        }
      )
    }

    bulmaJS.confirm({
      title: 'Delete Status',
      message: 'Are you sure you want to delete this status?',
      contextualColorName: 'warning',
      okButton: {
        text: 'Yes, Delete Status',
        callbackFunction: doDeleteStatus
      }
    })
  }

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

    let editStatusCloseModalFunction: () => void

    const index = Number.parseInt(
      (clickEvent.currentTarget as HTMLButtonElement).dataset.index ?? '-1',
      10
    )

    // eslint-disable-next-line security/detect-object-injection
    const statusObject = statusList[index]

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

      cityssm.postJSON(
        `${pts.urlPrefix}/tickets/doUpdateStatus`,
        formEvent.currentTarget,
        (rawResponseJSON) => {
          const responseJSON = rawResponseJSON as { success: boolean }
          if (responseJSON.success) {
            editStatusCloseModalFunction()
            getStatusesFunction()
          }
        }
      )
    }

    function statusKeyChangeFunction(changeEvent: Event): void {
      const statusKeyObject = pts.getTicketStatus(
        (changeEvent.currentTarget as HTMLSelectElement).value
      )

      const statusFieldInputElement = document.querySelector(
        '#editStatus--statusField'
      ) as HTMLInputElement
      statusFieldInputElement.value = ''

      const statusFieldFieldElement = statusFieldInputElement.closest(
        '.field'
      ) as HTMLElement

      if (statusKeyObject?.statusField) {
        ;(
          statusFieldFieldElement.querySelector('label') as HTMLLabelElement
        ).textContent = statusKeyObject.statusField.fieldLabel
        statusFieldFieldElement.classList.remove('is-hidden')
      } else {
        statusFieldFieldElement.classList.add('is-hidden')
      }

      const statusField2InputElement = document.querySelector(
        '#editStatus--statusField2'
      ) as HTMLInputElement
      statusField2InputElement.value = ''

      const statusField2FieldElement = statusField2InputElement.closest(
        '.field'
      ) as HTMLElement

      if (statusKeyObject?.statusField2) {
        ;(
          statusField2FieldElement.querySelector('label') as HTMLLabelElement
        ).textContent = statusKeyObject.statusField2.fieldLabel
        statusField2FieldElement.classList.remove('is-hidden')
      } else {
        statusField2FieldElement.classList.add('is-hidden')
      }
    }

    cityssm.openHtmlModal('ticket-editStatus', {
      onshow(modalElement) {
        ;(
          document.querySelector('#editStatus--ticketId') as HTMLInputElement
        ).value = ticketId
        ;(
          document.querySelector('#editStatus--statusIndex') as HTMLInputElement
        ).value = (statusObject.statusIndex as number).toString()
        ;(
          document.querySelector('#editStatus--statusField') as HTMLInputElement
        ).value = statusObject.statusField ?? ''
        ;(
          document.querySelector(
            '#editStatus--statusField2'
          ) as HTMLInputElement
        ).value = statusObject.statusField2 ?? ''
        ;(
          document.querySelector(
            '#editStatus--statusNote'
          ) as HTMLTextAreaElement
        ).value = statusObject.statusNote ?? ''

        const statusDateElement = document.querySelector(
          '#editStatus--statusDateString'
        ) as HTMLInputElement
        statusDateElement.value = statusObject.statusDateString ?? ''
        statusDateElement.setAttribute('max', cityssm.dateToString(new Date()))
        ;(
          document.querySelector(
            '#editStatus--statusTimeString'
          ) as HTMLInputElement
        ).value = statusObject.statusTimeString ?? ''

        pts.getDefaultConfigProperty(
          'parkingTicketStatuses',
          (parkingTicketStatuses: ConfigParkingTicketStatus[]) => {
            let statusKeyFound = false

            const statusKeyElement = document.querySelector(
              '#editStatus--statusKey'
            ) as HTMLSelectElement

            for (const statusKeyObject of parkingTicketStatuses) {
              if (
                statusKeyObject.isUserSettable ||
                statusKeyObject.statusKey === statusObject.statusKey
              ) {
                statusKeyElement.insertAdjacentHTML(
                  'beforeend',
                  `<option value="${statusKeyObject.statusKey}">${statusKeyObject.status}</option>`
                )

                if (statusKeyObject.statusKey === statusObject.statusKey) {
                  statusKeyFound = true

                  if (statusKeyObject.statusField) {
                    const fieldElement = document
                      .querySelector('#editStatus--statusField')
                      ?.closest('.field') as HTMLElement

                    ;(
                      fieldElement.querySelector('label') as HTMLLabelElement
                    ).textContent = statusKeyObject.statusField.fieldLabel
                    fieldElement.classList.remove('is-hidden')
                  }

                  if (statusKeyObject.statusField2) {
                    const fieldElement = document
                      .querySelector('#editStatus--statusField2')
                      ?.closest('.field') as HTMLElement

                    ;(
                      fieldElement.querySelector('label') as HTMLLabelElement
                    ).textContent = statusKeyObject.statusField2.fieldLabel
                    fieldElement.classList.remove('is-hidden')
                  }
                }
              }
            }

            if (!statusKeyFound) {
              statusKeyElement.insertAdjacentHTML(
                'beforeend',
                `<option value="${statusObject.statusKey}">${statusObject.statusKey}</option>`
              )
            }

            statusKeyElement.value = statusObject.statusKey ?? ''

            statusKeyElement.addEventListener('change', statusKeyChangeFunction)
          }
        )

        modalElement.querySelector('form')?.addEventListener('submit', doSubmit)
      },
      onshown(modalElement, closeModalFunction) {
        bulmaJS.toggleHtmlClipped()
        editStatusCloseModalFunction = closeModalFunction
        ;(
          modalElement.querySelector(
            '#editStatus--statusKey'
          ) as HTMLSelectElement
        ).focus()
      },
      onremoved() {
        bulmaJS.toggleHtmlClipped()
      }
    })
  }

  function populateStatusesPanelFunction(): void {
    clearStatusPanelFunction()

    if (statusList.length === 0) {
      statusPanelElement.insertAdjacentHTML(
        'beforeend',
        `<div class="panel-block is-block">
          <div class="message is-info">
          <p class="message-body">There are no statuses associated with this ticket.</p>
          </div>
          </div>`
      )

      return
    }

    // Loop through statuses
    for (const statusObject of statusList) {
      const statusDefinitionObject = pts.getTicketStatus(
        statusObject.statusKey ?? ''
      )

      const panelBlockElement = document.createElement('div')
      panelBlockElement.className = 'panel-block is-block'

      panelBlockElement.innerHTML = `<div class="columns">
        <div class="column">
        <div class="level mb-1">
          <div class="level-left">
          <strong>
          ${
            statusDefinitionObject
              ? statusDefinitionObject.status
              : statusObject.statusKey
          }
          </strong>
          </div>
          <div class="level-right">
            ${statusObject.statusDateString}
          </div>
        </div>
        ${
          !statusObject.statusField || statusObject.statusField === ''
            ? ''
            : `<p class="is-size-7">
                <strong>${
                  statusDefinitionObject?.statusField
                    ? statusDefinitionObject.statusField.fieldLabel
                    : ''
                }:</strong> ${statusObject.statusField}</p>`
        }
        ${
          !statusObject.statusField2 || statusObject.statusField2 === ''
            ? ''
            : `<p class="is-size-7">
                <strong>
                ${
                  statusDefinitionObject?.statusField2
                    ? statusDefinitionObject.statusField2.fieldLabel
                    : ''
                }:</strong> ${statusObject.statusField2}
                </p>`
        }
        <p class="has-newline-chars is-size-7">
          ${statusObject.statusNote}
        </p>
        </div>
        </div>`

      statusPanelElement.append(panelBlockElement)
    }

    // Initialize edit and delete buttons (if applicable)
    const firstStatusObject = statusList[0]

    if (firstStatusObject.canUpdate) {
      const firstStatusColumnsElement = statusPanelElement
        .querySelector('.panel-block')
        ?.querySelector('.columns') as HTMLElement

      firstStatusColumnsElement.insertAdjacentHTML(
        'beforeend',
        `<div class="column is-narrow">
          <div class="buttons is-right has-addons">
            <button class="button is-small is-edit-status-button" data-tooltip="Edit Status" data-index="0" type="button">
              <span class="icon is-small"><i class="fas fa-pencil-alt" aria-hidden="true"></i></span>
              <span>Edit</span>
            </button>
            <button class="button is-small has-text-danger is-delete-status-button" data-tooltip="Delete Status" data-status-index="${firstStatusObject.statusIndex.toString()}" type="button">
              <i class="fas fa-trash" aria-hidden="true"></i>
              <span class="sr-only">Delete</span>
            </button>
          </div>
          </div>`
      )

      firstStatusColumnsElement
        .querySelector('.is-edit-status-button')
        ?.addEventListener('click', openEditStatusModal)

      firstStatusColumnsElement
        .querySelector('.is-delete-status-button')
        ?.addEventListener('click', confirmDeleteStatusFunction)
    }

    // Add finalize button
    const firstStatusDefinitionObject = pts.getTicketStatus(
      firstStatusObject.statusKey
    )

    if (firstStatusDefinitionObject?.isFinalStatus) {
      const finalizePanelBlockElement = document.createElement('div')
      finalizePanelBlockElement.className = 'panel-block is-block'

      finalizePanelBlockElement.innerHTML = `<div class="message is-info is-clearfix">
        <div class="message-body">
          <div class="columns">
            <div class="column">
              <strong>This ticket is able to be marked as resolved.</strong>
            </div>
            <div class="column is-narrow has-text-right align-self-flex-end">
              <button class="button is-info" data-cy="resolve" type="button">
                <span class="icon is-small"><i class="fas fa-check" aria-hidden="true"></i></span>
                <span>Resolve Ticket</span>
              </button>
            </div>
          </div>
        </div>
        </div>`

      finalizePanelBlockElement
        .querySelector('button')
        ?.addEventListener('click', confirmResolveTicketFunction)

      statusPanelElement.prepend(finalizePanelBlockElement)
    }
  }

  function getStatusesFunction(): void {
    clearStatusPanelFunction()

    statusPanelElement.insertAdjacentHTML(
      'beforeend',
      `<div class="panel-block is-block">
        <p class="has-text-centered has-text-grey-lighter">
          <i class="fas fa-2x fa-circle-notch fa-spin" aria-hidden="true"></i><br />
          <em>Loading statuses...</em>
        </p>
        </div>`
    )

    cityssm.postJSON(
      `${pts.urlPrefix}/tickets/doGetStatuses`,
      {
        ticketId
      },
      (rawResponseJSON) => {
        const responseStatusList =
          rawResponseJSON as unknown as ParkingTicketStatusLog[]
        statusList = responseStatusList
        populateStatusesPanelFunction()
      }
    )
  }

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

      let addStatusCloseModalFunction: () => void

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

        const resolveTicket = (
          document.querySelector(
            '#addStatus--resolveTicket'
          ) as HTMLInputElement
        ).checked

        cityssm.postJSON(
          `${pts.urlPrefix}/tickets/doAddStatus`,
          formEvent.currentTarget,
          (rawResponseJSON) => {
            const responseJSON = rawResponseJSON as { success: boolean }
            if (responseJSON.success) {
              addStatusCloseModalFunction()

              if (resolveTicket) {
                window.location.href = `${pts.urlPrefix}/tickets/${ticketId}`
              } else {
                getStatusesFunction()
              }
            }
          }
        )
      }

      function statusKeyChangeFunction(changeEvent: Event): void {
        const statusObject = pts.getTicketStatus(
          (changeEvent.currentTarget as HTMLInputElement).value
        )

        const statusFieldInputElement = document.querySelector(
          '#addStatus--statusField'
        ) as HTMLInputElement
        statusFieldInputElement.value = ''

        const statusFieldFieldElement = statusFieldInputElement.closest(
          '.field'
        ) as HTMLElement

        if (statusObject?.statusField) {
          ;(
            statusFieldFieldElement.querySelector('label') as HTMLLabelElement
          ).textContent = statusObject.statusField.fieldLabel
          statusFieldFieldElement.classList.remove('is-hidden')
        } else {
          statusFieldFieldElement.classList.add('is-hidden')
        }

        const statusField2InputElement = document.querySelector(
          '#addStatus--statusField2'
        ) as HTMLInputElement
        statusField2InputElement.value = ''

        const statusField2FieldElement = statusField2InputElement.closest(
          '.field'
        ) as HTMLElement

        if (statusObject?.statusField2) {
          ;(
            statusField2FieldElement.querySelector('label') as HTMLLabelElement
          ).textContent = statusObject.statusField2.fieldLabel
          statusField2FieldElement.classList.remove('is-hidden')
        } else {
          statusField2FieldElement.classList.add('is-hidden')
        }

        const resolveTicketElement = document.querySelector(
          '#addStatus--resolveTicket'
        ) as HTMLInputElement
        resolveTicketElement.checked = false

        if (statusObject?.isFinalStatus) {
          resolveTicketElement.closest('.field')?.classList.remove('is-hidden')
        } else {
          resolveTicketElement.closest('.field')?.classList.add('is-hidden')
        }
      }

      cityssm.openHtmlModal('ticket-addStatus', {
        onshow(modalElement) {
          ;(
            document.querySelector('#addStatus--ticketId') as HTMLInputElement
          ).value = ticketId

          pts.getDefaultConfigProperty(
            'parkingTicketStatuses',
            (parkingTicketStatuses) => {
              const statusKeyElement = document.querySelector(
                '#addStatus--statusKey'
              ) as HTMLSelectElement

              for (const statusObject of parkingTicketStatuses as ConfigParkingTicketStatus[]) {
                if (statusObject.isUserSettable) {
                  statusKeyElement.insertAdjacentHTML(
                    'beforeend',
                    `<option value="${statusObject.statusKey}">${statusObject.status}</option>`
                  )
                }
              }

              statusKeyElement.addEventListener(
                'change',
                statusKeyChangeFunction
              )
            }
          )

          modalElement
            .querySelector('form')
            ?.addEventListener('submit', submitFunction)
        },
        onshown(modalElement, closeModalFunction) {
          bulmaJS.toggleHtmlClipped()
          addStatusCloseModalFunction = closeModalFunction
          ;(
            modalElement.querySelector(
              '#addStatus--statusKey'
            ) as HTMLSelectElement
          ).focus()
        },
        onremoved() {
          bulmaJS.toggleHtmlClipped()
          ;(
            document.querySelector('#is-add-status-button') as HTMLButtonElement
          ).focus()
        }
      })
    })

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

      let addPaidStatusCloseModalFunction: () => void

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

        const resolveTicket = (
          document.querySelector(
            '#addPaidStatus--resolveTicket'
          ) as HTMLInputElement
        ).checked

        cityssm.postJSON(
          `${pts.urlPrefix}/tickets/doAddStatus`,
          formEvent.currentTarget,
          (rawResponseJSON) => {
            const responseJSON = rawResponseJSON as { success: boolean }
            if (responseJSON.success) {
              addPaidStatusCloseModalFunction()

              if (resolveTicket) {
                window.location.href = `${pts.urlPrefix}/tickets/${ticketId}`
              } else {
                getStatusesFunction()
              }
            }
          }
        )
      }

      cityssm.openHtmlModal('ticket-addStatusPaid', {
        onshow(modalElement) {
          ;(
            document.querySelector(
              '#addPaidStatus--ticketId'
            ) as HTMLInputElement
          ).value = ticketId

          // Set amount

          const statusFieldElement = document.querySelector(
            '#addPaidStatus--statusField'
          ) as HTMLInputElement

          const offenceAmount = (
            document.querySelector('#ticket--offenceAmount') as HTMLInputElement
          ).value

          const issueDateString = (
            document.querySelector(
              '#ticket--issueDateString'
            ) as HTMLInputElement
          ).value

          const discountDays = (
            document.querySelector('#ticket--discountDays') as HTMLInputElement
          ).value

          if (issueDateString === '' || discountDays === '') {
            statusFieldElement.value = offenceAmount
          } else {
            const currentDateString = cityssm.dateToString(new Date())

            const dateDifference = cityssm.dateStringDifferenceInDays(
              issueDateString,
              currentDateString
            )

            statusFieldElement.value =
              dateDifference <= Number.parseInt(discountDays, 10)
                ? (
                    document.querySelector(
                      '#ticket--discountOffenceAmount'
                    ) as HTMLInputElement
                  ).value
                : offenceAmount
          }

          modalElement
            .querySelector('form')
            ?.addEventListener('submit', submitFunction)
        },
        onshown(modalElement, closeModalFunction) {
          bulmaJS.toggleHtmlClipped()
          addPaidStatusCloseModalFunction = closeModalFunction
          ;(
            modalElement.querySelector(
              '#addPaidStatus--statusField2'
            ) as HTMLInputElement
          ).focus()
        },
        onremoved() {
          bulmaJS.toggleHtmlClipped()
        }
      })
    })

  pts.loadDefaultConfigProperties(populateStatusesPanelFunction)
})()